Compare commits

..

1 Commits

Author SHA1 Message Date
NickVs2015
f07bc2132e fix: keystore write error fallback 2026-05-15 17:14:07 +03:00
388 changed files with 25842 additions and 70162 deletions

View File

@@ -18,17 +18,14 @@ jobs:
- uses: dorny/paths-filter@v3
id: filter
with:
base: ${{ github.event.before }}
filters: |
recipes:
- 'recipes/**'
- 'conanfile.py'
- '.github/workflows/deploy.yml'
- 'cmake/conan_provider.cmake'
- 'cmake/platform_settings.cmake'
- 'cmake/recipes_bootstrap.cmake'
Bake-Prebuilts-Linux:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
needs: Detect-Changes
if: needs.Detect-Changes.outputs.recipes_changed == 'true'
@@ -43,7 +40,7 @@ jobs:
python-version: 3.14
- name: 'Install conan'
run: pip install "conan==2.28.0"
run: pip install "conan==2.26.2"
- name: 'Build dependencies'
shell: bash
@@ -53,17 +50,15 @@ jobs:
done
- name: 'Authorize in remote'
if: github.ref == 'refs/heads/dev'
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
- name: 'Upload baked prebuilts'
if: github.ref == 'refs/heads/dev'
run: conan upload -r amnezia "*" -c
# ------------------------------------------------------
Build-Linux-Ubuntu:
runs-on: ubuntu-22.04
runs-on: android-runner
needs: Bake-Prebuilts-Linux
if: ${{ always() }}
@@ -103,7 +98,7 @@ jobs:
python-version: 3.14
- name: 'Install conan'
run: pip install "conan==2.28.0"
run: pip install "conan==2.26.2"
- name: 'Install system packages'
run: sudo apt-get install libxkbcommon-x11-0 libsecret-1-dev
@@ -123,7 +118,7 @@ jobs:
- name: 'Upload installer artifact'
uses: actions/upload-artifact@v7
with:
path: deploy/build/AmneziaVPN_*_linux_x64.run
path: deploy/build/AmneziaVPN-*-Linux.run
archive: false
retention-days: 7
@@ -154,17 +149,15 @@ jobs:
- uses: ilammy/msvc-dev-cmd@v1
- name: 'Install conan'
run: pip install "conan==2.28.0"
run: pip install "conan==2.26.2"
- name: 'Build dependencies'
run: cmake -S . -B build -DPREBUILTS_ONLY=1
run: cmake -S . -B build -G "Visual Studio 17 2022" -DPREBUILTS_ONLY=1
- name: 'Authorize in remote'
if: github.ref == 'refs/heads/dev'
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
- name: 'Upload baked prebuilts'
if: github.ref == 'refs/heads/dev'
run: conan upload -r amnezia "*" -c
# ------------------------------------------------------
@@ -236,7 +229,7 @@ jobs:
python-version: 3.14
- name: 'Install conan'
run: pip install "conan==2.28.0"
run: pip install "conan==2.26.2"
- name: 'Build project'
shell: cmd
@@ -249,31 +242,27 @@ jobs:
- name: 'Upload WIX installer artifact'
uses: actions/upload-artifact@v7
with:
path: deploy/build/AmneziaVPN_*_windows_x64.msi
path: deploy/build/AmneziaVPN-*-win64.msi
archive: false
retention-days: 7
- name: 'Upload IFW installer artifact'
uses: actions/upload-artifact@v7
with:
path: deploy/build/AmneziaVPN_*_windows_x64.exe
path: deploy/build/AmneziaVPN-*-win64.exe
archive: false
retention-days: 7
# ------------------------------------------------------
Bake-Prebuilts-iOS:
runs-on: macos-latest
needs: Detect-Changes
if: needs.Detect-Changes.outputs.recipes_changed == 'true'
strategy:
matrix:
xcode-version: [26.0, 26.4]
include:
- xcode-version: 26.4
os: macos-26
runs-on: ${{ matrix.os || 'macos-latest' }}
xcode-version: [26.0]
steps:
- uses: actions/checkout@v4
@@ -290,17 +279,15 @@ jobs:
xcode-version: ${{ matrix.xcode-version }}
- name: 'Install conan'
run: pip install "conan==2.28.0"
run: pip install "conan==2.26.2"
- name: 'Build dependencies'
run: cmake -S . -B build -G Xcode -DPREBUILTS_ONLY=1 -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_SYSROOT=iphoneos
- name: 'Authorize in remote'
if: github.ref == 'refs/heads/dev'
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
- name: 'Upload baked prebuilts'
if: github.ref == 'refs/heads/dev'
run: conan upload -r amnezia "*" -c
# ------------------------------------------------------
@@ -357,7 +344,7 @@ jobs:
- name: 'Setup xcode'
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '26.0'
xcode-version: '26.1'
- name: 'Install desktop Qt'
uses: jurplel/install-qt-action@v3
@@ -389,7 +376,7 @@ jobs:
python-version: 3.14
- name: 'Install deps'
run: pip install "conan==2.28.0" jsonschema jinja2
run: pip install "conan==2.26.2" jsonschema jinja2
- name: 'Build project'
env:
@@ -407,21 +394,14 @@ jobs:
# ------------------------------------------------------
Bake-Prebuilts-MacOS:
runs-on: macos-latest
needs: Detect-Changes
if: needs.Detect-Changes.outputs.recipes_changed == 'true'
strategy:
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
runs-on: ${{ matrix.os || 'macos-latest' }}
xcode-version: [16.2, 16.4]
steps:
- uses: actions/checkout@v4
@@ -438,23 +418,21 @@ jobs:
xcode-version: ${{ matrix.xcode-version }}
- name: 'Install conan'
run: pip install "conan==2.28.0"
run: pip install "conan==2.26.2"
- name: 'Build dependencies'
run: cmake -S . -B build -G Xcode -DPREBUILTS_ONLY=1
- name: 'Authorize in remote'
if: github.ref == 'refs/heads/dev'
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
- name: 'Upload baked prebuilts'
if: github.ref == 'refs/heads/dev'
run: conan upload -r amnezia "*" -c
# ------------------------------------------------------
Build-MacOS:
runs-on: macos-15
runs-on: macos-latest
needs: Bake-Prebuilts-MacOS
if: ${{ always() }}
@@ -501,7 +479,7 @@ jobs:
- name: 'Setup xcode'
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '26.3.0'
xcode-version: '16.2.0'
- name: 'Install Qt'
uses: jurplel/install-qt-action@v4
@@ -524,15 +502,13 @@ jobs:
python-version: 3.14
- name: 'Install conan'
run: pip install "conan==2.28.0"
run: pip install "conan==2.26.2"
- name: 'Build project'
env:
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 }}
@@ -542,28 +518,20 @@ jobs:
- name: 'Upload installer artifact'
uses: actions/upload-artifact@v7
with:
path: deploy/build/AmneziaVPN_*_macos_x64.pkg
path: deploy/build/AmneziaVPN-*-Darwin.pkg
archive: false
retention-days: 7
# ------------------------------------------------------
Bake-Prebuilts-MacOS-NE:
runs-on: macos-latest
needs: Detect-Changes
if: needs.Detect-Changes.outputs.recipes_changed == 'true'
strategy:
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
runs-on: ${{ matrix.os || 'macos-latest' }}
xcode-version: [16.2, 16.4]
steps:
- uses: actions/checkout@v4
@@ -580,23 +548,21 @@ jobs:
xcode-version: ${{ matrix.xcode-version }}
- name: 'Install conan'
run: pip install "conan==2.28.0"
run: pip install "conan==2.26.2"
- name: 'Build dependencies'
run: cmake -S . -B build -G Xcode -DPREBUILTS_ONLY=1 -DMACOS_NE=TRUE
- name: 'Authorize in remote'
if: github.ref == 'refs/heads/dev'
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
- name: 'Upload baked prebuilts'
if: github.ref == 'refs/heads/dev'
run: conan upload -r amnezia "*" -c
# ------------------------------------------------------
Build-MacOS-NE:
runs-on: macos-26
runs-on: macos-latest
needs: Bake-Prebuilts-MacOS-NE
if: ${{ always() }}
@@ -620,12 +586,6 @@ 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 }}
@@ -663,13 +623,19 @@ 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:
python-version: 3.14
- name: 'Install conan'
run: pip install "conan==2.28.0"
run: pip install "conan==2.26.2"
- name: 'Build project'
run: |
@@ -705,7 +671,7 @@ jobs:
python-version: 3.14
- name: 'Install conan'
run: pip install "conan==2.28.0"
run: pip install "conan==2.26.2"
- name: 'Setup Android SDK'
uses: android-actions/setup-android@v4
@@ -730,11 +696,9 @@ jobs:
done
- name: 'Authorize in remote'
if: github.ref == 'refs/heads/dev'
run: conan remote login amnezia "${{ secrets.CONAN_USER }}" -p "${{ secrets.CONAN_PASSWORD }}"
- name: 'Upload baked prebuilts'
if: github.ref == 'refs/heads/dev'
run: conan upload -r amnezia "*" -c
# ------------------------------------------------------
@@ -745,37 +709,11 @@ jobs:
needs: Bake-Prebuilts-Android
if: ${{ always() }}
strategy:
fail-fast: false
matrix:
include:
- label: 'android9-10'
qt_version: '6.10.1'
min_sdk: 28
max_sdk: 29
version_code_offset: 0
build_aab: true
- label: 'android11+'
qt_version: '6.10.3'
min_sdk: 30
max_sdk: ''
version_code_offset: 1
build_aab: true
- label: 'android9-10-qt610-3'
qt_version: '6.10.3'
min_sdk: 28
max_sdk: 29
version_code_offset: 0
build_aab: false
env:
ANDROID_PLATFORM: android-${{ matrix.min_sdk }}
ANDROID_PLATFORM: android-28
NDK_VERSION: 27.0.11718014
QT_VERSION: ${{ matrix.qt_version }}
QT_VERSION: 6.10.1
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
APP_ANDROID_MIN_SDK: ${{ matrix.min_sdk }}
APP_ANDROID_MAX_SDK: ${{ matrix.max_sdk }}
APP_ANDROID_VERSION_CODE_OFFSET: ${{ matrix.version_code_offset }}
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
@@ -868,7 +806,7 @@ jobs:
python-version: 3.14
- name: 'Install conan'
run: pip install "conan==2.28.0"
run: pip install "conan==2.26.2"
- name: 'Decode keystore secret to file'
env:
@@ -883,82 +821,62 @@ jobs:
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 }}
LABEL: ${{ matrix.label }}
BUILD_AAB: ${{ matrix.build_aab }}
shell: bash
run: |
OUT=deploy/artifacts
rm -rf $OUT && mkdir -p $OUT
if [[ "$BUILD_AAB" == "true" ]]; then
deploy/build.sh -t android --sign --aab
else
deploy/build.sh -t android --sign
fi
deploy/build.sh -t android --sign --aab
VERSION=$(grep CMAKE_PROJECT_VERSION:STATIC deploy/build/CMakeCache.txt | cut -d= -f2)
mv deploy/build/client/android-build/AmneziaVPN.apk \
$OUT/AmneziaVPN_${VERSION}_${LABEL}_universal.apk
if [[ "$BUILD_AAB" == "true" ]]; then
mv deploy/build/client/android-build/build/outputs/bundle/release/android-build-release.aab \
$OUT/AmneziaVPN_${VERSION}_${LABEL}.aab
fi
(cd deploy/build/client/android-build && mv AmneziaVPN.apk AmneziaVPN_${VERSION}_android9+_universal.apk)
(cd deploy/build/client/android-build/build/outputs/bundle/release && mv android-build-release.aab AmneziaVPN_${VERSION}.aab)
for abi in arm64-v8a armeabi-v7a x86 x86_64; do
deploy/build.sh -t android --sign --abi ${abi} --build ./deploy/build/${abi}
mv deploy/build/${abi}/client/android-build/AmneziaVPN.apk \
$OUT/AmneziaVPN_${VERSION}_${LABEL}_${abi}.apk
(cd deploy/build/${abi}/client/android-build && mv AmneziaVPN.apk AmneziaVPN_${VERSION}_android9+_${abi}.apk)
done
ls -l $OUT
- name: 'Upload universal APK'
uses: actions/upload-artifact@v7
with:
path: deploy/artifacts/*_universal.apk
path: deploy/build/client/android-build/*.apk
archive: false
retention-days: 7
- name: 'Upload AAB'
uses: actions/upload-artifact@v7
with:
path: deploy/build/client/android-build/build/outputs/bundle/release/*.aab
archive: false
retention-days: 7
- name: 'Upload arm64-v8a APK'
uses: actions/upload-artifact@v7
with:
path: deploy/artifacts/*_arm64-v8a.apk
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/artifacts/*_armeabi-v7a.apk
path: deploy/build/armeabi-v7a/client/android-build/*.apk
archive: false
retention-days: 7
- name: 'Upload x86 APK'
uses: actions/upload-artifact@v7
with:
path: deploy/artifacts/*_x86.apk
path: deploy/build/x86/client/android-build/*.apk
archive: false
retention-days: 7
- name: 'Upload x86_64 APK'
uses: actions/upload-artifact@v7
with:
path: deploy/artifacts/*_x86_64.apk
path: deploy/build/x86_64/client/android-build/*.apk
archive: false
retention-days: 7
- name: 'Upload AAB'
if: ${{ matrix.build_aab }}
uses: actions/upload-artifact@v7
with:
path: deploy/artifacts/*.aab
archive: false
retention-days: 7
# ------------------------------------------------------
Extra:
runs-on: ubuntu-latest
steps:
@@ -976,4 +894,3 @@ jobs:
run: |
echo "Pull request:" >> $GITHUB_STEP_SUMMARY
echo "[[#${{ fromJSON(steps.pull_request.outputs.data)[0].number }}] ${{ fromJSON(steps.pull_request.outputs.data)[0].title }}](${{ fromJSON(steps.pull_request.outputs.data)[0].html_url }})" >> $GITHUB_STEP_SUMMARY

2
.gitignore vendored
View File

@@ -10,8 +10,6 @@ deploy/build_64/*
winbuild*.bat
.cache/
.vscode/
.venv/
.cursor*
# Qt-es

3
.gitmodules vendored
View File

@@ -7,6 +7,9 @@
[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,9 +4,7 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(PROJECT AmneziaVPN)
if(NOT AMNEZIAVPN_VERSION)
set(AMNEZIAVPN_VERSION 5.0.1.1 CACHE STRING "Client app version")
endif()
set(AMNEZIAVPN_VERSION 4.8.15.4)
set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
set(CMAKE_PROJECT_TOP_LEVEL_INCLUDES
@@ -20,9 +18,9 @@ project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
HOMEPAGE_URL "https://amnezia.org/"
)
# trigger conan to kick off `conan install` globally
find_package(OpenSSL REQUIRED)
if (PREBUILTS_ONLY)
# trigger conan to kick off `conan install`
find_package(OpenSSL REQUIRED)
return()
endif()
@@ -30,12 +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})
# bump by 2 on every release, because we're releasing two versions on the play store
set(APP_ANDROID_VERSION_CODE 2148)
if(DEFINED APP_ANDROID_VERSION_CODE_OFFSET)
math(EXPR APP_ANDROID_VERSION_CODE "${APP_ANDROID_VERSION_CODE} + ${APP_ANDROID_VERSION_CODE_OFFSET}")
endif()
set(APP_ANDROID_VERSION_CODE 2120)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux")
@@ -61,14 +54,14 @@ if(APPLE AND NOT IOS)
endif()
configure_file(
"${CMAKE_CURRENT_SOURCE_DIR}/deploy/data/pf-templates/amn.400.allowPIA.conf.in"
"${CMAKE_SOURCE_DIR}/deploy/data/pf-templates/amn.400.allowPIA.conf.in"
"${CMAKE_CURRENT_BINARY_DIR}/amn.400.allowPIA.conf"
@ONLY
)
file(COPY_FILE
"${CMAKE_CURRENT_BINARY_DIR}/amn.400.allowPIA.conf"
"${CMAKE_CURRENT_SOURCE_DIR}/deploy/data/macos/pf/amn.400.allowPIA.conf"
"${CMAKE_SOURCE_DIR}/deploy/data/macos/pf/amn.400.allowPIA.conf"
ONLY_IF_DIFFERENT
)
endif()

View File

@@ -38,14 +38,14 @@
## Links
- [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://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://www.reddit.com/r/AmneziaVPN](https://www.reddit.com/r/AmneziaVPN) - Reddit
- [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)
- [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
## 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/) и XRay.
- Протоколы с маскировкой трафика (обфускацией): OpenVPN с плагином [Cloak](https://github.com/cbeuw/Cloak), Shadowsocks (OpenVPN over Shadowsocks), [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) and 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/?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://amnezia.org](https://amnezia.org) - Веб-сайт проекта | [Альтернативная ссылка (зеркало)](https://storage.googleapis.com/kldscp/amnezia.org)
- [https://docs.amnezia.org](https://docs.amnezia.org) - Документация
- [https://www.reddit.com/r/AmneziaVPN](https://www.reddit.com/r/AmneziaVPN) - Reddit
- [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)
- [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\)
## Технологии

1
client/3rd/QSimpleCrypto vendored Submodule

View File

@@ -1,8 +1,6 @@
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
include(${CMAKE_CURRENT_LIST_DIR}/cmake/clientBrandingDefaults.cmake)
set(PROJECT ${CLIENT_TARGET_NAME})
set(PROJECT AmneziaVPN)
project(${PROJECT})
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
@@ -41,7 +39,6 @@ if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
endif()
find_package(Qt6 REQUIRED COMPONENTS ${PACKAGES})
find_package(OpenSSL REQUIRED)
set(LIBS ${LIBS}
Qt6::Core Qt6::Gui
@@ -59,9 +56,6 @@ qt_add_executable(${PROJECT} MANUAL_FINALIZATION)
target_include_directories(${PROJECT} PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>
)
target_compile_definitions(${PROJECT} PRIVATE
CLIENT_KEYCHAIN_NAME_STR=\"${CLIENT_KEYCHAIN_NAME}\"
)
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_interface.rep)
@@ -72,34 +66,24 @@ qt6_add_resources(QRC ${QRC}
${CMAKE_CURRENT_LIST_DIR}/images/images.qrc
${CMAKE_CURRENT_LIST_DIR}/images/flagKit.qrc
${CMAKE_CURRENT_LIST_DIR}/client_scripts/clientScripts.qrc
${CLIENT_QML_QRC_FILE}
${CMAKE_CURRENT_LIST_DIR}/ui/qml/qml.qrc
${CMAKE_CURRENT_LIST_DIR}/server_scripts/serverScripts.qrc
${CLIENT_EXTRA_QRC}
)
# -- i18n begin
if(CLIENT_TS_FILES)
set(AMNEZIAVPN_TS_FILES ${CLIENT_TS_FILES})
else()
set(AMNEZIAVPN_TS_FILES
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_ru_RU.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_zh_CN.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_fa_IR.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_ar_EG.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_my_MM.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_uk_UA.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_ur_PK.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_hi_IN.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_es_ES.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_ko_KR.ts
)
endif()
file(GLOB_RECURSE AMNEZIAVPN_QML_FILES ${CMAKE_CURRENT_LIST_DIR}/ui/qml/*.qml)
set(AMNEZIAVPN_TS_FILES
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_ru_RU.ts
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_zh_CN.ts
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_fa_IR.ts
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_ar_EG.ts
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_my_MM.ts
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_uk_UA.ts
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_ur_PK.ts
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_hi_IN.ts
)
qt6_add_translations(${PROJECT}
TS_FILES ${AMNEZIAVPN_TS_FILES}
SOURCES ${AMNEZIAVPN_QML_FILES}
RESOURCE_PREFIX "/translations"
)
# -- i18n end
@@ -116,18 +100,12 @@ endif()
include(${CMAKE_CURRENT_LIST_DIR}/cmake/3rdparty.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/cmake/sources.cmake)
set(_client_include_dirs
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}
)
if(CLIENT_BRANDING_INCLUDE_DIR)
list(APPEND _client_include_dirs ${CLIENT_BRANDING_INCLUDE_DIR})
endif()
list(APPEND _client_include_dirs ${CMAKE_CURRENT_LIST_DIR})
include_directories(${_client_include_dirs})
if(MACOS_NE)
message("MACOS_NE is ON")
@@ -143,26 +121,6 @@ include_directories(mozilla/shared)
include_directories(mozilla/models)
configure_file(${CMAKE_CURRENT_LIST_DIR}/../version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h)
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/core/utils)
configure_file(
${CMAKE_CURRENT_LIST_DIR}/core/utils/appUiConfig.h.in
${CMAKE_CURRENT_BINARY_DIR}/core/utils/appUiConfig.h
@ONLY
)
configure_file(
${CMAKE_CURRENT_LIST_DIR}/core/utils/swiftBridgeConfig.h.in
${CMAKE_CURRENT_BINARY_DIR}/core/utils/swiftBridgeConfig.h
@ONLY
)
# Swift can't consume a `-DGROUP_ID="value"` compile define (Swift -D flags are
# presence-only), unlike the Objective-C++ sources that read GROUP_ID directly.
# Generate a Swift constant instead so Log.swift can use the branded app group.
set(CLIENT_GROUP_IDENTIFIER_SWIFT_FILE ${CMAKE_CURRENT_BINARY_DIR}/platforms/ios/GroupIdentifier.swift)
configure_file(
${CMAKE_CURRENT_LIST_DIR}/platforms/ios/GroupIdentifier.swift.in
${CLIENT_GROUP_IDENTIFIER_SWIFT_FILE}
@ONLY
)
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
target_compile_definitions(${PROJECT} PRIVATE "MZ_DEBUG")
@@ -215,7 +173,6 @@ endif()
if(LINUX AND NOT ANDROID)
set(LIBS ${LIBS} -static-libstdc++ -static-libgcc -ldl)
link_directories(${CMAKE_CURRENT_LIST_DIR}/platforms/linux)
set(LIBS ${LIBS} -Wl,--no-as-needed $<TARGET_LINKER_FILE:Qt6::RemoteObjects> -Wl,--as-needed)
endif()
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
@@ -236,6 +193,10 @@ elseif(APPLE)
include(cmake/macos.cmake)
endif()
if(NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
add_subdirectory(tests)
endif()
list(APPEND SOURCES ${CMAKE_CURRENT_LIST_DIR}/main.cpp)
target_link_libraries(${PROJECT} PRIVATE ${LIBS})
@@ -255,32 +216,11 @@ endif()
install(TARGETS ${PROJECT}
DESTINATION ${CMAKE_INSTALL_BINDIR}
RUNTIME_DEPENDENCY_SET client_deps
COMPONENT AmneziaVPN
)
if(APPLE)
set(RUNTIME_DEPS_DIR ${CMAKE_INSTALL_BINDIR}/AmneziaVPN.app/Contents/Frameworks)
else()
set(RUNTIME_DEPS_DIR ${CMAKE_INSTALL_BINDIR})
endif()
install(RUNTIME_DEPENDENCY_SET client_deps
PRE_EXCLUDE_REGEXES
[[api-ms-win-.*]]
[[ext-ms-.*]]
[[kernel32\.dll]]
[[hvsifiletrust\.dll]]
[[libc\.so\..*]] [[libgcc_s\.so\..*]] [[libm\.so\..*]] [[libstdc\+\+\.so\..*]]
[[.*\.framework]]
[[^(lib)?[Qq]t.*]]
POST_EXCLUDE_REGEXES
[[^.*[\\/]system32[\\/].*\.dll$]]
[[^/lib.*]]
[[^/usr/lib.*]]
DIRECTORIES ${CONAN_RUNTIME_LIB_DIRS}
install(FILES $<TARGET_RUNTIME_DLLS:${PROJECT}>
DESTINATION ${CMAKE_INSTALL_BINDIR}
COMPONENT AmneziaVPN
DESTINATION "${RUNTIME_DEPS_DIR}"
)
set(deploy_tool_options "")
@@ -300,10 +240,6 @@ 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,14 +22,9 @@
#include "logger.h"
#include "ui/controllers/qml/pageController.h"
#include "ui/models/installedAppsModel.h"
#include "ui/utils/mtProxyPublicHostInput.h"
#include "version.h"
#include "core/utils/appUiConfig.h"
#include "platforms/ios/QRCodeReaderBase.h"
#ifdef Q_OS_IOS
#include "platforms/ios/ioscontextmenu.h"
#endif
bool AmneziaApplication::m_forceQuit = false;
@@ -103,7 +98,7 @@ void AmneziaApplication::init()
{
m_engine = new QQmlApplicationEngine;
const QUrl url(QStringLiteral(APP_QML_ENTRYPOINT));
const QUrl url(QStringLiteral("qrc:/ui/qml/main2.qml"));
QObject::connect(
m_engine, &QQmlApplicationEngine::objectCreated, this,
[this, url](QObject *obj, const QUrl &objUrl) {
@@ -124,13 +119,7 @@ void AmneziaApplication::init()
win->setPersistentSceneGraph(true);
win->setPersistentGraphics(true);
#endif
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
win->show();
#else
if (!m_coreController || !m_coreController->pageController()->shouldStartMinimized()) {
win->show();
}
#endif
}
},
Qt::QueuedConnection);
@@ -143,20 +132,13 @@ void AmneziaApplication::init()
m_engine->rootContext()->setContextProperty("IsMacOsNeBuild", false);
#endif
#ifdef Q_OS_IOS
m_engine->rootContext()->setContextProperty("IosContextMenu", new IosContextMenu(this));
#endif
m_vpnConnection.reset(new VpnConnection(nullptr, nullptr));
m_vpnConnection->moveToThread(&m_vpnConnectionThread);
m_vpnConnectionThread.start();
m_coreController.reset(new CoreController(m_vpnConnection, m_settings, m_engine));
m_marketplaceUpdateController.reset(new MarketplaceUpdateController());
m_marketplaceUpdateController->start();
m_engine->addImportPath(QStringLiteral(APP_QML_IMPORT_PATH));
m_engine->addImportPath("qrc:/ui/qml/Modules/");
if (m_parser.isSet(m_optImport)) {
const QString data = m_parser.value(m_optImport);
@@ -233,9 +215,6 @@ 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();
@@ -245,7 +224,7 @@ void AmneziaApplication::loadFonts()
{
QQuickStyle::setStyle("Basic");
QFontDatabase::addApplicationFont(QStringLiteral(APP_UI_FONT_RESOURCE));
QFontDatabase::addApplicationFont(":/fonts/pt-root-ui_vf.ttf");
}
bool AmneziaApplication::parseCommands()
@@ -272,7 +251,7 @@ bool AmneziaApplication::parseCommands()
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
void AmneziaApplication::startLocalServer() {
const QString serverName(APP_INSTANCE_NAME);
const QString serverName("AmneziaVPNInstance");
QLocalServer::removeServer(serverName);
QLocalServer *server = new QLocalServer(this);

View File

@@ -15,7 +15,6 @@
#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"
@@ -57,7 +56,6 @@ private:
SecureQSettings* m_settings;
QScopedPointer<CoreController> m_coreController;
QScopedPointer<MarketplaceUpdateController> m_marketplaceUpdateController;
QSharedPointer<ContainerProps> m_containerProps;
QSharedPointer<ProtocolProps> m_protocolProps;

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:type="linear"
android:angle="135"
android:startColor="#2A2A2E"
android:centerColor="#17171A"
android:endColor="#0E0E11" />
</shape>

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ic_amnezia_round"
android:insetLeft="19.5%"
android:insetTop="19.5%"
android:insetRight="19.5%"
android:insetBottom="19.5%" />

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="NoActionBar">
<item name="android:windowBackground">@color/black</item>
<item name="android:colorBackground">@color/black</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:enforceNavigationBarContrast">false</item>
<item name="android:enforceStatusBarContrast">false</item>
<item name="android:windowSplashScreenBackground">@color/ic_launcher_background</item>
<item name="android:windowSplashScreenIconBackgroundColor">@color/ic_launcher_background</item>
<item name="android:windowSplashScreenAnimatedIcon">@mipmap/icon</item>
</style>
</resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#0E0E11</color>
</resources>

View File

@@ -3,13 +3,6 @@ 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
@@ -49,7 +42,6 @@ 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
@@ -107,8 +99,6 @@ 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) {
@@ -496,116 +486,6 @@ 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")
@@ -883,13 +763,7 @@ class AmneziaActivity : QtActivity() {
fun openFile(filter: String?) {
Log.v(TAG, "Open file with filter: $filter")
mainScope.launch {
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 intent = if (!isOnTv()) {
val mimeTypes = if (!filter.isNullOrEmpty()) {
val extensionRegex = "\\*\\.([a-z0-9]+)".toRegex(IGNORE_CASE)
val mime = MimeTypeMap.getSingleton()
@@ -915,7 +789,6 @@ class AmneziaActivity : QtActivity() {
else -> type = "*/*"
}
}
`package` = systemPickerPackage
}
} else {
Intent(this@AmneziaActivity, TvFilePicker::class.java)
@@ -927,11 +800,8 @@ class AmneziaActivity : QtActivity() {
if (isOnTv() && it?.hasExtra("activityNotFound") == true) {
showNoFileBrowserAlertDialog()
}
val uri = it?.data?.let { u ->
if (u.scheme == "content") {
try { grantUriPermission(packageName, u, Intent.FLAG_GRANT_READ_URI_PERMISSION) } catch (_: Exception) {}
}
u
val uri = it?.data?.apply {
grantUriPermission(packageName, this, Intent.FLAG_GRANT_READ_URI_PERMISSION)
}?.toString() ?: ""
Log.v(TAG, "Open file: $uri")
if (uri.isNotEmpty()) {
@@ -971,12 +841,7 @@ class AmneziaActivity : QtActivity() {
Log.v(TAG, "Get fd for $fileName")
return blockingCall(Dispatchers.IO) {
try {
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 = contentResolver.openFileDescriptor(Uri.parse(fileName), "r")
pfd?.fd ?: -1
} catch (e: Exception) {
Log.e(TAG, "Failed to get fd: $e")
@@ -1196,10 +1061,12 @@ class AmneziaActivity : QtActivity() {
@Suppress("unused")
fun sendTouch(x: Float, y: Float) {
Log.v(TAG, "Send touch: $x, $y")
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))
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))
}
}
}

View File

@@ -1,36 +1,30 @@
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() {
// SAF launcher for Android 10+ where File API is blocked by scoped storage
private val safLauncher = registerForActivityResult(object : ActivityResultContracts.OpenDocument() {
private val fileChooseResultLauncher = registerForActivityResult(object : ActivityResultContracts.OpenDocument() {
override fun createIntent(context: Context, input: Array<String>): Intent {
val intent = super.createIntent(context, input)
val activities = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val activitiesToResolveIntent = 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 (activities.all {
if (activitiesToResolveIntent.all {
val name = it.activityInfo.packageName
name.startsWith("com.google.android.tv.frameworkpackagestubs") || name.startsWith("com.android.tv.frameworkpackagestubs")
}) {
@@ -38,140 +32,38 @@ class TvFilePicker : ComponentActivity() {
}
return intent
}
}) { uri ->
}) {
setResult(RESULT_OK, Intent().apply {
data = uri
data = it
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")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
launchSaf()
} else {
checkPermissionAndBrowse()
}
getFile()
}
@Deprecated("Deprecated in Java")
override fun onBackPressed() {
navigateBack()
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
Log.v(TAG, "onNewIntent")
getFile()
}
private fun launchSaf() {
private fun getFile() {
try {
safLauncher.launch(arrayOf("*/*"))
Log.v(TAG, "getFile")
fileChooseResultLauncher.launch(arrayOf("*/*"))
} catch (_: ActivityNotFoundException) {
Log.w(TAG, "No SAF activity found")
Log.w(TAG, "Activity not found")
setResult(RESULT_CANCELED, Intent().apply { putExtra("activityNotFound", true) })
finish()
} catch (e: Exception) {
Log.e(TAG, "SAF launch failed: $e")
Log.e(TAG, "Failed to get file: $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) }
configData.optStringOrNull("persistent_keep_alive")?.let { setPersistentKeepalive(it.toInt()) }
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,24 +134,6 @@ 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) }
configData.optStringOrNull("RandomTrailers")?.trim()?.takeIf { it.isNotEmpty() }
?.let { setRandomTrailers(it) }
configData.optStringOrNull("DisableCookies")?.trim()?.takeIf { it.isNotEmpty() }
?.let { setDisableCookies(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: String?,
val persistentKeepalive: Int,
val publicKeyHex: String,
val preSharedKeyHex: String?,
val privateKeyHex: String,
@@ -31,15 +31,6 @@ 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?,
val randomTrailers: String?,
val disableCookies: String?,
) : ProtocolConfig(protocolConfigBuilder) {
protected constructor(builder: Builder) : this(
@@ -66,15 +57,6 @@ 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,
builder.randomTrailers,
builder.disableCookies,
)
fun toWgUserspaceString(): String = with(StringBuilder()) {
@@ -105,15 +87,6 @@ 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") }
randomTrailers?.takeIf { it.isNotEmpty() }?.let { appendLine("random_trailers=${it.toUapiBool()}") }
disableCookies?.takeIf { it.isNotEmpty() }?.let { appendLine("disable_cookies=${it.toUapiBool()}") }
}
private fun validateProtocolExtensionParameters() {
@@ -134,7 +107,7 @@ open class WireguardConfig protected constructor(
appendLine("allowed_ip=${route.inetNetwork}")
}
appendLine("endpoint=$endpoint")
if (!persistentKeepalive.isNullOrEmpty() && persistentKeepalive != "0")
if (persistentKeepalive != 0)
appendLine("persistent_keepalive_interval=$persistentKeepalive")
if (preSharedKeyHex != null)
appendLine("preshared_key=$preSharedKeyHex")
@@ -144,7 +117,7 @@ open class WireguardConfig protected constructor(
internal lateinit var endpoint: InetEndpoint
private set
internal var persistentKeepalive: String? = null
internal var persistentKeepalive: Int = 0
private set
internal lateinit var publicKeyHex: String
@@ -176,19 +149,10 @@ 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
internal var randomTrailers: String? = null
internal var disableCookies: String? = null
fun setEndpoint(endpoint: InetEndpoint) = apply { this.endpoint = endpoint }
fun setPersistentKeepalive(persistentKeepalive: String) = apply { this.persistentKeepalive = persistentKeepalive }
fun setPersistentKeepalive(persistentKeepalive: Int) = apply { this.persistentKeepalive = persistentKeepalive }
fun setPublicKeyHex(publicKeyHex: String) = apply { this.publicKeyHex = publicKeyHex }
@@ -214,15 +178,6 @@ 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 }
fun setRandomTrailers(randomTrailers: String) = apply { this.randomTrailers = randomTrailers }
fun setDisableCookies(disableCookies: String) = apply { this.disableCookies = disableCookies }
override fun build(): WireguardConfig = configBuild().run { WireguardConfig(this@Builder) }
}
@@ -234,9 +189,3 @@ open class WireguardConfig protected constructor(
@OptIn(ExperimentalStdlibApi::class)
internal fun String.base64ToHex(): String = Base64.decode(this, Base64.DEFAULT).toHexString()
/** Converts awg-quick on/off (and 0/1/true/false) to UAPI 1/0 for amneziawg-go ParseBool. */
internal fun String.toUapiBool(): String = when (trim().lowercase()) {
"on", "1", "true", "t", "yes" -> "1"
else -> "0"
}

View File

@@ -2,8 +2,9 @@ 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 ${CMAKE_BINARY_DIR}/3rd/SortFilterProxyModel)
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/SortFilterProxyModel)
set(LIBS ${LIBS} SortFilterProxyModel)
include(${CLIENT_ROOT_DIR}/cmake/QSimpleCrypto.cmake)
include(${CLIENT_ROOT_DIR}/3rd/qrcodegen/qrcodegen.cmake)
@@ -11,24 +12,20 @@ add_compile_definitions(_WINSOCKAPI_)
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
set(BUILD_WITH_QT6 ON)
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtkeychain ${CMAKE_BINARY_DIR}/3rd/qtkeychain EXCLUDE_FROM_ALL)
add_subdirectory(${CLIENT_ROOT_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 ${CMAKE_BINARY_DIR}/3rd/qtgamepad)
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtgamepad)
# Link both the C++ module and QML plugin
if(TARGET ${PROJECT})
if(TARGET GamepadLegacy)
target_link_libraries(${PROJECT} PRIVATE GamepadLegacy)
else()
list(APPEND LIBS GamepadLegacy)
endif()
if(TARGET ${PROJECT})
if(TARGET GamepadLegacyQuickPrivate)
target_link_libraries(${PROJECT} PRIVATE GamepadLegacyQuickPrivate)
else()
list(APPEND LIBS GamepadLegacyQuickPrivate)
endif()
message(STATUS "Gamepad support enabled for Android")
else()
@@ -39,6 +36,7 @@ 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

@@ -0,0 +1,21 @@
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

@@ -1,8 +1,6 @@
message("Client android ${CMAKE_ANDROID_ARCH_ABI} build")
if(NOT DEFINED APP_ANDROID_MIN_SDK)
set(APP_ANDROID_MIN_SDK 28)
endif()
set(APP_ANDROID_MIN_SDK 28)
set(ANDROID_PLATFORM "android-${APP_ANDROID_MIN_SDK}" CACHE STRING
"The minimum API level supported by the application or library" FORCE)
@@ -15,6 +13,7 @@ set_target_properties(${PROJECT} PROPERTIES
QT_ANDROID_MIN_SDK_VERSION ${APP_ANDROID_MIN_SDK}
QT_ANDROID_TARGET_SDK_VERSION 36
QT_ANDROID_SDK_BUILD_TOOLS_REVISION 36.0.0
QT_ANDROID_PACKAGE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/android
)
set(QT_ANDROID_MULTI_ABI_FORWARD_VARS "QT_NO_GLOBAL_APK_TARGET_PART_OF_ALL;CMAKE_BUILD_TYPE")
@@ -54,27 +53,3 @@ file(COPY ${AMNEZIA_LIBXRAY_PATH} DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/androi
find_package(openvpn-pt-android REQUIRED)
set(LIBS ${LIBS} amnezia::openvpn-pt-android)
set_property(TARGET ${PROJECT} APPEND PROPERTY QT_ANDROID_EXTRA_LIBS ${OPENVPN_PT_ANDROID_LIBCK_OVPN_PLUGIN_PATH})
set(APP_ANDROID_PACKAGE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/android)
if(APP_ANDROID_MAX_SDK)
set(APP_ANDROID_PACKAGE_SOURCE_DIR ${CMAKE_CURRENT_BINARY_DIR}/android-package-source)
file(REMOVE_RECURSE ${APP_ANDROID_PACKAGE_SOURCE_DIR})
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/android/ DESTINATION ${APP_ANDROID_PACKAGE_SOURCE_DIR})
set(manifest_path ${APP_ANDROID_PACKAGE_SOURCE_DIR}/AndroidManifest.xml)
set(manifest_anchor "android:installLocation=\"auto\">")
file(READ ${manifest_path} manifest_contents)
string(REPLACE
"${manifest_anchor}"
"${manifest_anchor}\n\n <uses-sdk android:maxSdkVersion=\"${APP_ANDROID_MAX_SDK}\" />"
patched_contents "${manifest_contents}")
if(patched_contents STREQUAL manifest_contents)
message(FATAL_ERROR
"Failed to set maxSdkVersion=${APP_ANDROID_MAX_SDK}: anchor '${manifest_anchor}' "
"not found in ${CMAKE_CURRENT_SOURCE_DIR}/android/AndroidManifest.xml")
endif()
file(WRITE ${manifest_path} "${patched_contents}")
endif()
set_property(TARGET ${PROJECT} PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR ${APP_ANDROID_PACKAGE_SOURCE_DIR})

View File

@@ -1,3 +0,0 @@
if(NOT CLIENT_ANDROID_PACKAGE)
set(CLIENT_ANDROID_PACKAGE "org.amnezia.vpn" CACHE STRING "Android package name for Play Store version lookup")
endif()

View File

@@ -1,9 +0,0 @@
if(NOT CLIENT_SWIFT_MODULE_NAME)
set(CLIENT_SWIFT_MODULE_NAME "${CLIENT_APPLICATION_NAME}" CACHE STRING "Swift module name for Objective-C interop header")
endif()
if(NOT CLIENT_SWIFT_OBJC_HEADER_NAME)
set(CLIENT_SWIFT_OBJC_HEADER_NAME "${CLIENT_SWIFT_MODULE_NAME}-Swift.h" CACHE STRING "Generated Swift Objective-C header name")
endif()
if(NOT CLIENT_NETWORK_EXTENSION_NAME)
set(CLIENT_NETWORK_EXTENSION_NAME "${CLIENT_APPLICATION_NAME}NetworkExtension" CACHE STRING "Display name for Apple network extension targets")
endif()

View File

@@ -1,60 +0,0 @@
if(NOT CLIENT_TARGET_NAME)
set(CLIENT_TARGET_NAME "AmneziaVPN" CACHE STRING "Client executable target name")
endif()
if(NOT CLIENT_APPLICATION_NAME)
set(CLIENT_APPLICATION_NAME "AmneziaVPN" CACHE STRING "Application display and executable name")
endif()
if(NOT CLIENT_SERVICE_NAME)
set(CLIENT_SERVICE_NAME "AmneziaVPN-service" CACHE STRING "Service executable name")
endif()
if(NOT CLIENT_ORGANIZATION_NAME)
set(CLIENT_ORGANIZATION_NAME "AmneziaVPN.ORG" CACHE STRING "QSettings organization name")
endif()
if(NOT CLIENT_APP_INSTANCE_NAME)
set(CLIENT_APP_INSTANCE_NAME "AmneziaVPNInstance" CACHE STRING "Single-instance local server name")
endif()
if(NOT CLIENT_KEYCHAIN_NAME)
set(CLIENT_KEYCHAIN_NAME "${CLIENT_APPLICATION_NAME}-Keychain" CACHE STRING "QtKeychain service name used for encrypted settings keys")
endif()
# -- QML entry points and UI resources
if(NOT CLIENT_QML_ENTRYPOINT)
set(CLIENT_QML_ENTRYPOINT "qrc:/ui/qml/main2.qml" CACHE STRING "Main QML entrypoint URL")
endif()
if(NOT CLIENT_QML_IMPORT_PATH)
set(CLIENT_QML_IMPORT_PATH "qrc:/ui/qml/Modules/" CACHE STRING "Additional QQmlEngine import path")
endif()
if(NOT CLIENT_QML_PAGES_PREFIX)
set(CLIENT_QML_PAGES_PREFIX "qrc:/ui/qml/Pages2/" CACHE STRING "PageController QML pages prefix")
endif()
if(NOT CLIENT_UI_FONT_RESOURCE)
set(CLIENT_UI_FONT_RESOURCE ":/fonts/pt-root-ui_vf.ttf" CACHE STRING "UI font resource path")
endif()
if(NOT CLIENT_QML_QRC_FILE)
set(CLIENT_QML_QRC_FILE "${_CLIENT_SRC_DIR}/ui/qml/qml.qrc" CACHE FILEPATH "QML resources manifest")
endif()
if(NOT CLIENT_EXTRA_QRC)
set(CLIENT_EXTRA_QRC "" CACHE STRING "Optional semicolon-separated extra QRC files")
endif()
# -- translations
if(NOT CLIENT_TS_PREFIX)
set(CLIENT_TS_PREFIX "amneziavpn" CACHE STRING "Translation filename prefix")
endif()
if(NOT CLIENT_TS_FILES)
set(CLIENT_TS_FILES "" CACHE STRING "Optional semicolon-separated absolute TS file paths")
endif()
# -- brand-provided headers
if(NOT CLIENT_BRANDING_INCLUDE_DIR)
set(CLIENT_BRANDING_INCLUDE_DIR "" CACHE PATH "Optional include root for non-generated branding headers (e.g. pageEnum.h)")
endif()
if(CLIENT_BRANDING_INCLUDE_DIR AND EXISTS "${CLIENT_BRANDING_INCLUDE_DIR}/ui/utils/pageEnum.h")
set(CLIENT_PAGE_ENUM_HEADER "${CLIENT_BRANDING_INCLUDE_DIR}/ui/utils/pageEnum.h")
else()
set(CLIENT_PAGE_ENUM_HEADER "${_CLIENT_SRC_DIR}/ui/utils/pageEnum.h")
endif()
if(NOT CLIENT_NOTIFICATION_IDENTIFIER)
set(CLIENT_NOTIFICATION_IDENTIFIER "${CLIENT_TS_PREFIX}" CACHE STRING "Local notification request identifier")
endif()

View File

@@ -1,45 +0,0 @@
if(NOT CLIENT_IOS_NE_TARGET_NAME)
set(CLIENT_IOS_NE_TARGET_NAME "networkextension" CACHE STRING "iOS network extension build target name")
endif()
if(NOT CLIENT_IOS_NE_ENTITLEMENTS_FILE)
set(CLIENT_IOS_NE_ENTITLEMENTS_FILE "AmneziaVPNNetworkExtension.entitlements" CACHE STRING "iOS network extension entitlements filename")
endif()
if(NOT CLIENT_IOS_LAUNCHSCREEN_FILE)
set(CLIENT_IOS_LAUNCHSCREEN_FILE "AmneziaVPNLaunchScreen.storyboard" CACHE STRING "iOS launch screen storyboard filename")
endif()
get_filename_component(CLIENT_IOS_LAUNCHSCREEN_NAME "${CLIENT_IOS_LAUNCHSCREEN_FILE}" NAME_WE)
if(NOT CLIENT_IOS_APP_ENTITLEMENTS_PATH)
set(CLIENT_IOS_APP_ENTITLEMENTS_PATH "${_CLIENT_SRC_DIR}/ios/app/main.entitlements" CACHE FILEPATH "iOS app entitlements file path")
endif()
if(NOT CLIENT_IOS_NE_ENTITLEMENTS_PATH)
set(CLIENT_IOS_NE_ENTITLEMENTS_PATH "${_CLIENT_SRC_DIR}/ios/networkextension/${CLIENT_IOS_NE_ENTITLEMENTS_FILE}" CACHE FILEPATH "iOS network extension entitlements file path")
endif()
if(NOT CLIENT_IOS_LAUNCHSCREEN_PATH)
set(CLIENT_IOS_LAUNCHSCREEN_PATH "${_CLIENT_SRC_DIR}/ios/app/${CLIENT_IOS_LAUNCHSCREEN_FILE}" CACHE FILEPATH "iOS launch screen storyboard path")
endif()
if(NOT CLIENT_IOS_MEDIA_ASSETS_PATH)
set(CLIENT_IOS_MEDIA_ASSETS_PATH "${_CLIENT_SRC_DIR}/ios/app/Media.xcassets" CACHE FILEPATH "iOS app Media.xcassets path")
endif()
if(NOT CLIENT_IOS_BUNDLE_ID)
set(CLIENT_IOS_BUNDLE_ID "org.amnezia.AmneziaVPN" CACHE STRING "iOS bundle id for App Store version lookup")
endif()
if(NOT CLIENT_IOS_STORE_URL_FALLBACK)
set(CLIENT_IOS_STORE_URL_FALLBACK "itms-apps://itunes.apple.com/app/id1600529900" CACHE STRING "Fallback App Store URL when lookup omits trackViewUrl")
endif()
# -- signing
if(NOT CLIENT_IOS_PROVISIONING_PROFILE_SPECIFIER)
set(CLIENT_IOS_PROVISIONING_PROFILE_SPECIFIER "distr ios.org.amnezia.AmneziaVPN" CACHE STRING "iOS app provisioning profile specifier for release")
endif()
if(NOT CLIENT_IOS_PROVISIONING_PROFILE_SPECIFIER_DEBUG)
set(CLIENT_IOS_PROVISIONING_PROFILE_SPECIFIER_DEBUG "dev ios.org.amnezia.AmneziaVPN" CACHE STRING "iOS app provisioning profile specifier for debug")
endif()
if(NOT CLIENT_IOS_NE_PROVISIONING_PROFILE_SPECIFIER)
set(CLIENT_IOS_NE_PROVISIONING_PROFILE_SPECIFIER "distr ios.org.amnezia.AmneziaVPN" CACHE STRING "iOS network extension provisioning profile specifier for release")
endif()
if(NOT CLIENT_IOS_NE_PROVISIONING_PROFILE_SPECIFIER_DEBUG)
set(CLIENT_IOS_NE_PROVISIONING_PROFILE_SPECIFIER_DEBUG "dev ios.org.amnezia.AmneziaVPN" CACHE STRING "iOS network extension provisioning profile specifier for debug")
endif()

View File

@@ -1,37 +0,0 @@
if(NOT CLIENT_MACOS_NE_TARGET_NAME)
set(CLIENT_MACOS_NE_TARGET_NAME "${CLIENT_NETWORK_EXTENSION_NAME}" CACHE STRING "macOS network extension build target name")
endif()
if(NOT CLIENT_MACOS_NE_ENTITLEMENTS_FILE)
set(CLIENT_MACOS_NE_ENTITLEMENTS_FILE "AmneziaVPNNetworkExtension.entitlements" CACHE STRING "macOS network extension entitlements filename")
endif()
if(NOT CLIENT_MACOS_APP_ENTITLEMENTS_PATH)
set(CLIENT_MACOS_APP_ENTITLEMENTS_PATH "${_CLIENT_SRC_DIR}/macos/app/app.entitlements" CACHE FILEPATH "macOS app entitlements file path")
endif()
if(NOT CLIENT_MACOS_NE_ENTITLEMENTS_PATH)
set(CLIENT_MACOS_NE_ENTITLEMENTS_PATH "${_CLIENT_SRC_DIR}/macos/networkextension/${CLIENT_MACOS_NE_ENTITLEMENTS_FILE}" CACHE FILEPATH "macOS network extension entitlements file path")
endif()
if(NOT CLIENT_MACOS_MEDIA_ASSETS_PATH)
set(CLIENT_MACOS_MEDIA_ASSETS_PATH "${_CLIENT_SRC_DIR}/macos/app/Images.xcassets" CACHE FILEPATH "macOS app Images.xcassets path")
endif()
if(NOT CLIENT_MACOS_APP_ICNS_PATH)
set(CLIENT_MACOS_APP_ICNS_PATH "${_CLIENT_SRC_DIR}/images/app.icns" CACHE FILEPATH "macOS app icon (.icns) used by non-App Store builds")
endif()
if(NOT CLIENT_MACOS_EXTRA_PLIST_ENTRIES)
set(CLIENT_MACOS_EXTRA_PLIST_ENTRIES "" CACHE STRING "Extra macOS Info.plist entries appended verbatim (e.g. appearance pinning)")
endif()
# -- signing
if(NOT CLIENT_MACOS_PROVISIONING_PROFILE_SPECIFIER)
set(CLIENT_MACOS_PROVISIONING_PROFILE_SPECIFIER "distr macos.org.amnezia.AmneziaVPN" CACHE STRING "macOS app provisioning profile specifier for release")
endif()
if(NOT CLIENT_MACOS_PROVISIONING_PROFILE_SPECIFIER_DEBUG)
set(CLIENT_MACOS_PROVISIONING_PROFILE_SPECIFIER_DEBUG "dev macos.org.amnezia.AmneziaVPN" CACHE STRING "macOS app provisioning profile specifier for debug")
endif()
if(NOT CLIENT_MACOS_NE_PROVISIONING_PROFILE_SPECIFIER)
set(CLIENT_MACOS_NE_PROVISIONING_PROFILE_SPECIFIER "distr macos.org.amnezia.amneziaVPN.NE" CACHE STRING "macOS network extension provisioning profile specifier for release")
endif()
if(NOT CLIENT_MACOS_NE_PROVISIONING_PROFILE_SPECIFIER_DEBUG)
set(CLIENT_MACOS_NE_PROVISIONING_PROFILE_SPECIFIER_DEBUG "dev macos.org.amnezia.amneziaVPN.NE" CACHE STRING "macOS network extension provisioning profile specifier for debug")
endif()

View File

@@ -1,9 +0,0 @@
get_filename_component(_CLIENT_SRC_DIR "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
include(${CMAKE_CURRENT_LIST_DIR}/branding/common.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/branding/apple.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/branding/ios.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/branding/macos.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/branding/android.cmake)
unset(_CLIENT_SRC_DIR)

View File

@@ -24,17 +24,6 @@ set(IOS_TARGET_COMPILED_TYPES
EXECUTABLE
)
## Simulator architecture: honor CMAKE_OSX_ARCHITECTURES when set, otherwise
## match the host — arm64 Macs run arm64 simulators (Xcode 26 has no Rosetta
## simulator destinations), Intel Macs need x86_64.
if(CMAKE_OSX_ARCHITECTURES)
string(REPLACE ";" " " IOS_SIMULATOR_ARCHS "${CMAKE_OSX_ARCHITECTURES}")
elseif(CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "arm64")
set(IOS_SIMULATOR_ARCHS "arm64")
else()
set(IOS_SIMULATOR_ARCHS "x86_64")
endif()
## Inspect all the targets, and add extra properties if necessary.
while(IOS_TARGETS)
list(POP_FRONT IOS_TARGETS TARGET_NAME)
@@ -49,6 +38,6 @@ while(IOS_TARGETS)
message("Patching architectures for ${TARGET_NAME}")
set_target_properties(${TARGET_NAME} PROPERTIES
XCODE_ATTRIBUTE_ARCHS[sdk=iphoneos*] "arm64"
XCODE_ATTRIBUTE_ARCHS[sdk=iphonesimulator*] "${IOS_SIMULATOR_ARCHS}"
XCODE_ATTRIBUTE_ARCHS[sdk=iphonesimulator*] "x86_64"
)
endwhile()

View File

@@ -31,7 +31,6 @@ set(HEADERS ${HEADERS}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ioscontextmenu.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate-C-Interface.h
@@ -43,7 +42,6 @@ set(SOURCES ${SOURCES}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ioscontextmenu.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm
@@ -51,47 +49,43 @@ set(SOURCES ${SOURCES}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/AmneziaSceneDelegateHooks.mm
)
# The context menu helper uses ARC-only constructs (weak references); the
# rest of the Objective-C++ sources build with manual reference counting.
set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ioscontextmenu.mm
PROPERTIES COMPILE_OPTIONS "-fobjc-arc"
)
target_include_directories(${PROJECT} PRIVATE ${Qt6Gui_PRIVATE_INCLUDE_DIRS})
set_target_properties(${PROJECT} PROPERTIES
XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Info.plist.in
MACOSX_BUNDLE_ICON_FILE "AppIcon"
MACOSX_BUNDLE_INFO_STRING "${CLIENT_APPLICATION_NAME}"
MACOSX_BUNDLE_BUNDLE_NAME "${CLIENT_APPLICATION_NAME}"
MACOSX_BUNDLE_INFO_STRING "AmneziaVPN"
MACOSX_BUNDLE_BUNDLE_NAME "AmneziaVPN"
MACOSX_BUNDLE_GUI_IDENTIFIER "${BUILD_IOS_APP_IDENTIFIER}"
MACOSX_BUNDLE_BUNDLE_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
MACOSX_BUNDLE_LONG_VERSION_STRING "${APPLE_PROJECT_VERSION}-${CMAKE_PROJECT_VERSION_TWEAK}"
MACOSX_BUNDLE_SHORT_VERSION_STRING "${APPLE_PROJECT_VERSION}"
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "${BUILD_IOS_APP_IDENTIFIER}"
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "${CLIENT_IOS_APP_ENTITLEMENTS_PATH}"
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "${CMAKE_CURRENT_SOURCE_DIR}/ios/app/main.entitlements"
XCODE_ATTRIBUTE_MARKETING_VERSION "${APPLE_PROJECT_VERSION}"
XCODE_ATTRIBUTE_CURRENT_PROJECT_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
XCODE_ATTRIBUTE_PRODUCT_NAME "${CLIENT_APPLICATION_NAME}"
XCODE_ATTRIBUTE_BUNDLE_INFO_STRING "${CLIENT_APPLICATION_NAME}"
XCODE_ATTRIBUTE_PRODUCT_NAME "AmneziaVPN"
XCODE_ATTRIBUTE_BUNDLE_INFO_STRING "AmneziaVPN"
XCODE_GENERATE_SCHEME TRUE
XCODE_ATTRIBUTE_ENABLE_BITCODE "NO"
XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME "AppIcon"
XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2"
XCODE_EMBED_FRAMEWORKS_CODE_SIGN_ON_COPY ON
XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION
XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/Frameworks"
XCODE_EMBED_APP_EXTENSIONS ${CLIENT_IOS_NE_TARGET_NAME}
XCODE_EMBED_APP_EXTENSIONS networkextension
)
if(DEPLOY)
if(DEFINED DEPLOY)
set_target_properties(${PROJECT} PROPERTIES
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Apple Distribution"
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY[variant=Debug] "Apple Development"
XCODE_ATTRIBUTE_CODE_SIGN_STYLE Manual
XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER "${CLIENT_IOS_PROVISIONING_PROFILE_SPECIFIER}"
XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER[variant=Debug] "${CLIENT_IOS_PROVISIONING_PROFILE_SPECIFIER_DEBUG}"
XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER "distr ios.org.amnezia.AmneziaVPN"
XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER[variant=Debug] "dev ios.org.amnezia.AmneziaVPN"
)
else()
set_target_properties(${PROJECT} PROPERTIES
@@ -103,11 +97,11 @@ set_target_properties(${PROJECT} PROPERTIES
XCODE_ATTRIBUTE_SWIFT_VERSION "5.0"
XCODE_ATTRIBUTE_CLANG_ENABLE_MODULES "YES"
XCODE_ATTRIBUTE_SWIFT_PRECOMPILE_BRIDGING_HEADER "NO"
XCODE_ATTRIBUTE_SWIFT_OBJC_INTERFACE_HEADER_NAME "${CLIENT_SWIFT_OBJC_HEADER_NAME}"
XCODE_ATTRIBUTE_SWIFT_OBJC_INTERFACE_HEADER_NAME "AmneziaVPN-Swift.h"
XCODE_ATTRIBUTE_SWIFT_OBJC_INTEROP_MODE "objcxx"
)
set_target_properties(${PROJECT} PROPERTIES
XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "${BUILD_VPN_DEVELOPMENT_TEAM}"
XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "X7UJ388FXK"
)
target_include_directories(${PROJECT} PRIVATE ${CMAKE_CURRENT_LIST_DIR})
target_compile_options(${PROJECT} PRIVATE
@@ -122,28 +116,22 @@ target_sources(${PROJECT} PRIVATE
${WG_APPLE_SOURCE_DIR}/WireGuardKitC/x25519.c
${CLIENT_ROOT_DIR}/platforms/ios/LogController.swift
${CLIENT_ROOT_DIR}/platforms/ios/Log.swift
${CLIENT_GROUP_IDENTIFIER_SWIFT_FILE}
${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift
${CLIENT_ROOT_DIR}/platforms/ios/ScreenProtection.swift
${CLIENT_ROOT_DIR}/platforms/ios/VPNCController.swift
${CLIENT_ROOT_DIR}/platforms/ios/StoreKit2Helper.swift
)
set_source_files_properties(
${CLIENT_IOS_MEDIA_ASSETS_PATH}
PROPERTIES MACOSX_PACKAGE_LOCATION Resources
)
target_sources(${PROJECT} PRIVATE
${CLIENT_IOS_LAUNCHSCREEN_PATH}
${CLIENT_IOS_MEDIA_ASSETS_PATH}
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/AmneziaVPNLaunchScreen.storyboard
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Media.xcassets
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
)
set_property(TARGET ${PROJECT} APPEND PROPERTY RESOURCE
${CLIENT_IOS_LAUNCHSCREEN_PATH}
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/AmneziaVPNLaunchScreen.storyboard
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
)
add_subdirectory(ios/networkextension)
add_dependencies(${PROJECT} ${CLIENT_IOS_NE_TARGET_NAME})
add_dependencies(${PROJECT} networkextension)

View File

@@ -34,8 +34,8 @@ set(SOURCES ${SOURCES}
set(ICON_FILE ${CLIENT_MACOS_APP_ICNS_PATH})
get_filename_component(MACOSX_BUNDLE_ICON_FILE "${CLIENT_MACOS_APP_ICNS_PATH}" NAME)
set(ICON_FILE ${CMAKE_CURRENT_SOURCE_DIR}/images/app.icns)
set(MACOSX_BUNDLE_ICON_FILE app.icns)
set_source_files_properties(${ICON_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
set(SOURCES ${SOURCES} ${ICON_FILE})

View File

@@ -5,7 +5,6 @@ set_target_properties(${PROJECT} PROPERTIES MACOSX_BUNDLE TRUE)
set(APPLE_PROJECT_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
enable_language(OBJC)
enable_language(OBJCXX)
enable_language(Swift)
find_package(Qt6 REQUIRED COMPONENTS ShaderTools Widgets)
@@ -34,6 +33,7 @@ set(LIBS ${LIBS}
set(HEADERS ${HEADERS}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate-C-Interface.h
@@ -44,12 +44,19 @@ set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_contro
set(SOURCES ${SOURCES}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm
)
set(ICON_FILE ${CMAKE_CURRENT_SOURCE_DIR}/images/app.icns)
set(MACOSX_BUNDLE_ICON_FILE app.icns)
set_source_files_properties(${ICON_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
set(SOURCES ${SOURCES} ${ICON_FILE})
target_include_directories(${PROJECT} PRIVATE
${Qt6Gui_PRIVATE_INCLUDE_DIRS}
${Qt6Widgets_PRIVATE_INCLUDE_DIRS}
@@ -60,25 +67,28 @@ set_target_properties(${PROJECT} PROPERTIES
XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/macos/app/Info.plist.in
MACOSX_BUNDLE_ICON_FILE "AppIcon"
MACOSX_BUNDLE_INFO_STRING "${CLIENT_APPLICATION_NAME}"
MACOSX_BUNDLE_BUNDLE_NAME "${CLIENT_APPLICATION_NAME}"
MACOSX_BUNDLE_GUI_IDENTIFIER "${BUILD_IOS_APP_IDENTIFIER}"
MACOSX_BUNDLE_INFO_STRING "AmneziaVPN"
MACOSX_BUNDLE_BUNDLE_NAME "AmneziaVPN"
MACOSX_BUNDLE_BUNDLE_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
MACOSX_BUNDLE_LONG_VERSION_STRING "${APPLE_PROJECT_VERSION}-${CMAKE_PROJECT_VERSION_TWEAK}"
MACOSX_BUNDLE_SHORT_VERSION_STRING "${APPLE_PROJECT_VERSION}"
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "${BUILD_IOS_APP_IDENTIFIER}"
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "${CLIENT_MACOS_APP_ENTITLEMENTS_PATH}"
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "${CMAKE_CURRENT_SOURCE_DIR}/macos/app/app.entitlements"
XCODE_ATTRIBUTE_MARKETING_VERSION "${APPLE_PROJECT_VERSION}"
XCODE_ATTRIBUTE_CURRENT_PROJECT_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
XCODE_ATTRIBUTE_PRODUCT_NAME "${CLIENT_APPLICATION_NAME}"
XCODE_ATTRIBUTE_BUNDLE_INFO_STRING "${CLIENT_APPLICATION_NAME}"
XCODE_ATTRIBUTE_PRODUCT_NAME "AmneziaVPN"
XCODE_ATTRIBUTE_BUNDLE_INFO_STRING "AmneziaVPN"
XCODE_GENERATE_SCHEME TRUE
XCODE_ATTRIBUTE_ENABLE_BITCODE "NO"
XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME "AppIcon"
XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2"
XCODE_EMBED_FRAMEWORKS_CODE_SIGN_ON_COPY "NO"
XCODE_EMBED_FRAMEWORKS_REMOVE_HEADERS_ON_COPY "YES"
XCODE_ATTRIBUTE_MACOSX_DEPLOYMENT_TARGET "11.0"
XCODE_LINK_BUILD_PHASE_MODE KNOWN_LOCATION
XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/../Frameworks"
XCODE_EMBED_APP_EXTENSIONS ${CLIENT_MACOS_NE_TARGET_NAME}
XCODE_EMBED_APP_EXTENSIONS AmneziaVPNNetworkExtension
)
if(DEPLOY)
@@ -86,8 +96,8 @@ if(DEPLOY)
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Apple Distribution"
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY[variant=Debug] "Apple Development"
XCODE_ATTRIBUTE_CODE_SIGN_STYLE Manual
XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER "${CLIENT_MACOS_PROVISIONING_PROFILE_SPECIFIER}"
XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER[variant=Debug] "${CLIENT_MACOS_PROVISIONING_PROFILE_SPECIFIER_DEBUG}"
XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER "distr macos.org.amnezia.AmneziaVPN"
XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER[variant=Debug] "dev macos.org.amnezia.AmneziaVPN"
)
else()
set_target_properties(${PROJECT} PROPERTIES
@@ -99,11 +109,11 @@ set_target_properties(${PROJECT} PROPERTIES
XCODE_ATTRIBUTE_SWIFT_VERSION "5.0"
XCODE_ATTRIBUTE_CLANG_ENABLE_MODULES "YES"
XCODE_ATTRIBUTE_SWIFT_PRECOMPILE_BRIDGING_HEADER "NO"
XCODE_ATTRIBUTE_SWIFT_OBJC_INTERFACE_HEADER_NAME "${CLIENT_SWIFT_OBJC_HEADER_NAME}"
XCODE_ATTRIBUTE_SWIFT_OBJC_INTERFACE_HEADER_NAME "AmneziaVPN-Swift.h"
XCODE_ATTRIBUTE_SWIFT_OBJC_INTEROP_MODE "objcxx"
)
set_target_properties(${PROJECT} PROPERTIES
XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "${BUILD_VPN_DEVELOPMENT_TEAM}"
XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "X7UJ388FXK"
)
target_include_directories(${PROJECT} PRIVATE ${CMAKE_CURRENT_LIST_DIR})
target_compile_options(${PROJECT} PRIVATE
@@ -117,20 +127,14 @@ target_sources(${PROJECT} PRIVATE
${WG_APPLE_SOURCE_DIR}/WireGuardKitC/x25519.c
${CLIENT_ROOT_DIR}/platforms/ios/LogController.swift
${CLIENT_ROOT_DIR}/platforms/ios/Log.swift
${CLIENT_GROUP_IDENTIFIER_SWIFT_FILE}
${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift
${CLIENT_ROOT_DIR}/platforms/ios/ScreenProtection.swift
${CLIENT_ROOT_DIR}/platforms/ios/VPNCController.swift
${CLIENT_ROOT_DIR}/platforms/ios/StoreKit2Helper.swift
)
set_source_files_properties(
${CLIENT_MACOS_MEDIA_ASSETS_PATH}
PROPERTIES MACOSX_PACKAGE_LOCATION Resources
)
target_sources(${PROJECT} PRIVATE
${CLIENT_MACOS_MEDIA_ASSETS_PATH}
${CMAKE_CURRENT_SOURCE_DIR}/macos/app/Images.xcassets
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
)
@@ -139,7 +143,7 @@ set_property(TARGET ${PROJECT} APPEND PROPERTY RESOURCE
)
add_subdirectory(macos/networkextension)
add_dependencies(${PROJECT} ${CLIENT_MACOS_NE_TARGET_NAME})
add_dependencies(${PROJECT} AmneziaVPNNetworkExtension)
get_target_property(QtCore_location Qt6::Core LOCATION)
message("QtCore_location")
@@ -148,5 +152,5 @@ message(${QtCore_location})
get_filename_component(QT_BIN_DIR_DETECTED "${QtCore_location}/../../../../../bin" ABSOLUTE)
add_custom_command(TARGET ${PROJECT} POST_BUILD
COMMAND ${QT_BIN_DIR_DETECTED}/macdeployqt $<TARGET_BUNDLE_DIR:${PROJECT}> -appstore-compliant -qmldir=${CMAKE_CURRENT_SOURCE_DIR} -no-codesign
COMMAND ${QT_BIN_DIR_DETECTED}/macdeployqt $<TARGET_BUNDLE_DIR:AmneziaVPN> -appstore-compliant -qmldir=${CMAKE_CURRENT_SOURCE_DIR}
)

View File

@@ -35,8 +35,6 @@ set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/core/installers/torInstaller.h
${CLIENT_ROOT_DIR}/core/installers/sftpInstaller.h
${CLIENT_ROOT_DIR}/core/installers/socks5Installer.h
${CLIENT_ROOT_DIR}/core/installers/mtProxyInstaller.h
${CLIENT_ROOT_DIR}/core/installers/telemtInstaller.h
${CLIENT_ROOT_DIR}/core/controllers/appSplitTunnelingController.h
${CLIENT_ROOT_DIR}/core/controllers/ipSplitTunnelingController.h
${CLIENT_ROOT_DIR}/core/controllers/allowedDnsController.h
@@ -51,23 +49,17 @@ set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/core/repositories/secureAppSettingsRepository.h
${CLIENT_ROOT_DIR}/core/protocols/qmlRegisterProtocols.h
${CLIENT_ROOT_DIR}/ui/utils/pages.h
${CLIENT_PAGE_ENUM_HEADER}
${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/api/gatewayPayloadBuilder.h
${CLIENT_ROOT_DIR}/core/utils/osSignalHandler.h
${CLIENT_ROOT_DIR}/core/utils/swiftBridge.h
${CLIENT_ROOT_DIR}/core/utils/utilities.h
${CLIENT_ROOT_DIR}/core/utils/managementServer.h
${CLIENT_ROOT_DIR}/core/utils/constants.h
@@ -118,8 +110,6 @@ set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/core/installers/torInstaller.cpp
${CLIENT_ROOT_DIR}/core/installers/sftpInstaller.cpp
${CLIENT_ROOT_DIR}/core/installers/socks5Installer.cpp
${CLIENT_ROOT_DIR}/core/installers/mtProxyInstaller.cpp
${CLIENT_ROOT_DIR}/core/installers/telemtInstaller.cpp
${CLIENT_ROOT_DIR}/core/controllers/appSplitTunnelingController.cpp
${CLIENT_ROOT_DIR}/core/controllers/ipSplitTunnelingController.cpp
${CLIENT_ROOT_DIR}/core/controllers/allowedDnsController.cpp
@@ -133,11 +123,9 @@ 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
@@ -147,10 +135,8 @@ 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/api/gatewayPayloadBuilder.cpp
${CLIENT_ROOT_DIR}/core/utils/serverConfigUtils.cpp
${CLIENT_ROOT_DIR}/core/utils/osSignalHandler.cpp
${CLIENT_ROOT_DIR}/core/utils/utilities.cpp
@@ -215,14 +201,12 @@ file(GLOB UI_MODELS_H CONFIGURE_DEPENDS
${CLIENT_ROOT_DIR}/ui/models/*.h
${CLIENT_ROOT_DIR}/ui/models/protocols/*.h
${CLIENT_ROOT_DIR}/ui/models/services/*.h
${CLIENT_ROOT_DIR}/ui/models/utils/*.h
${CLIENT_ROOT_DIR}/ui/models/api/*.h
)
file(GLOB UI_MODELS_CPP CONFIGURE_DEPENDS
${CLIENT_ROOT_DIR}/ui/models/*.cpp
${CLIENT_ROOT_DIR}/ui/models/protocols/*.cpp
${CLIENT_ROOT_DIR}/ui/models/services/*.cpp
${CLIENT_ROOT_DIR}/ui/models/utils/*.cpp
${CLIENT_ROOT_DIR}/ui/models/api/*.cpp
)

View File

@@ -96,19 +96,13 @@ ProtocolConfig AwgConfigurator::createConfig(const ServerCredentials &credential
newClientConfig.specialJunk4 = configMap.value(configKey::specialJunk4);
newClientConfig.specialJunk5 = configMap.value(configKey::specialJunk5);
newClientConfig.cookieReplyPacketJunkSize = configMap.value(configKey::cookieReplyPacketJunkSize);
newClientConfig.transportPacketJunkSize = configMap.value(configKey::transportPacketJunkSize);
newClientConfig.headerProtectionKey = configMap.value(configKey::headerProtectionKey);
newClientConfig.contentPaddingAddition = configMap.value(configKey::contentPaddingAddition);
newClientConfig.rekeyAfterTime = configMap.value(configKey::rekeyAfterTime);
newClientConfig.rekeyTimeout = configMap.value(configKey::rekeyTimeout);
newClientConfig.rejectAfterTime = configMap.value(configKey::rejectAfterTime);
newClientConfig.keepaliveTimeout = configMap.value(configKey::keepaliveTimeout);
newClientConfig.maxHandshakeAttempts = configMap.value(configKey::maxHandshakeAttempts);
newClientConfig.randomTrailers = configMap.value(configKey::randomTrailers);
newClientConfig.disableCookies = configMap.value(configKey::disableCookies);
if (container == DockerContainer::Awg2) {
newClientConfig.cookieReplyPacketJunkSize = configMap.value(configKey::cookieReplyPacketJunkSize);
newClientConfig.transportPacketJunkSize = configMap.value(configKey::transportPacketJunkSize);
}
newClientConfig.isObfuscationEnabled = false;
protocolConfig.setClientConfig(newClientConfig);
return protocolConfig;

View File

@@ -233,13 +233,6 @@ ProtocolConfig WireguardConfigurator::createConfig(const ServerCredentials &cred
QString scriptData = amnezia::scriptData(m_configTemplate, container);
QString config = m_sshSession->replaceVars(scriptData, vars);
// The template lists every possible key, but each parameter is optional -
// drop the lines whose value came out empty
static const QRegularExpression emptyValueLine(R"(^\s*\S+\s*=\s*$)");
auto configTemplateLines = config.split("\n");
configTemplateLines.removeIf([](const QString &line) { return emptyValueLine.match(line).hasMatch(); });
config = configTemplateLines.join("\n");
ConnectionData connData = prepareWireguardConfig(credentials, container, wireguardServerConfig, awgServerConfig, dnsSettings, errorCode);
if (errorCode != ErrorCode::NoError) {
return WireGuardProtocolConfig{};
@@ -273,9 +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" };
const bool useKeepAliveRange = awgServerConfig && awgServerConfig->hasAwg3Params();
clientConfig.persistentKeepAlive = useKeepAliveRange ? protocols::awg::defaultPersistentKeepAlive
: protocols::wireguard::defaultPersistentKeepAlive;
clientConfig.persistentKeepAlive = "25";
clientConfig.mtu = mtu;
clientConfig.isObfuscationEnabled = false;

View File

@@ -4,8 +4,6 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QDateTime>
#include <QThread>
#include <QUuid>
#include "logger.h"
@@ -22,775 +20,183 @@
#include "core/models/protocols/xrayProtocolConfig.h"
namespace {
Logger logger("XrayConfigurator");
QString effectiveClientFlow(const amnezia::XrayServerConfig &srv)
{
return amnezia::xrayEffective::clientFlow(srv);
}
QString effectiveSecurity(const amnezia::XrayServerConfig &srv)
{
return amnezia::xrayEffective::security(srv);
}
// Desktop applies this in XrayProtocol::start(); iOS/Android pass JSON straight to libxray — same fixes here.
void sanitizeXrayNativeConfig(amnezia::ProtocolConfig &pc)
{
QString c = pc.nativeConfig();
if (c.isEmpty()) {
return;
}
bool changed = false;
if (c.contains(QLatin1String("Mozilla/5.0"), Qt::CaseInsensitive)) {
c.replace(QLatin1String("Mozilla/5.0"), QString::fromLatin1(amnezia::protocols::xray::defaultFingerprint),
Qt::CaseInsensitive);
changed = true;
}
const QString legacyListen = QString::fromLatin1(amnezia::protocols::xray::defaultLocalAddr);
const QString listenOk = QString::fromLatin1(amnezia::protocols::xray::defaultLocalListenAddr);
if (c.contains(legacyListen)) {
c.replace(legacyListen, listenOk);
changed = true;
}
if (changed) {
pc.setNativeConfig(c);
}
}
QString tlsFingerprintFromEnsureOutput(const QString &stdOut)
{
const QString prefix = QStringLiteral("amnezia_tls_fp=");
const auto lines = stdOut.split(QLatin1Char('\n'));
for (QString line : lines) {
line = line.trimmed();
if (!line.startsWith(prefix))
continue;
QString fp = line.mid(prefix.size());
fp.remove(QLatin1Char(':'));
fp.remove(QLatin1Char(' '));
fp = fp.toLower();
if (fp.size() != 64)
return {};
for (const QChar ch : fp) {
const ushort u = ch.unicode();
const bool hex = (u >= '0' && u <= '9') || (u >= 'a' && u <= 'f');
if (!hex)
return {};
}
return fp;
}
return {};
}
} // namespace
Logger logger("XrayConfigurator");
}
XrayConfigurator::XrayConfigurator(SshSession* sshSession, QObject *parent)
: ConfiguratorBase(sshSession, parent)
{
}
amnezia::ProtocolConfig XrayConfigurator::processConfigWithLocalSettings(const amnezia::ConnectionSettings &settings,
amnezia::ProtocolConfig protocolConfig)
{
applyDnsToNativeConfig(settings.dns, protocolConfig);
sanitizeXrayNativeConfig(protocolConfig);
return protocolConfig;
}
ErrorCode XrayConfigurator::uploadServerConfigJson(const ServerCredentials &credentials, DockerContainer container,
const DnsSettings &dnsSettings, const QJsonObject &serverConfig) const
{
const QString updatedConfig = QJsonDocument(serverConfig).toJson();
ErrorCode errorCode = m_sshSession->uploadTextFileToContainer(
container, credentials, updatedConfig, amnezia::protocols::xray::serverConfigPath,
libssh::ScpOverwriteMode::ScpOverwriteExisting);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to upload updated config";
return errorCode;
}
const QString restartScript = QStringLiteral("sudo docker restart $CONTAINER_NAME");
errorCode = m_sshSession->runScript(
credentials,
m_sshSession->replaceVars(restartScript,
amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns,
dnsSettings.secondaryDns)));
if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to restart container";
}
return errorCode;
}
ErrorCode XrayConfigurator::readRealityKeyFiles(const DockerContainer container, const ServerCredentials &credentials,
QString &outPublicKey, QString &outShortId) const
{
outPublicKey.clear();
outShortId.clear();
auto readKeyFile = [&](const QString &path, QString &out) -> ErrorCode {
for (int attempt = 0; attempt < 3; ++attempt) {
ErrorCode fileError = ErrorCode::NoError;
out = QString::fromUtf8(m_sshSession->getTextFileFromContainer(container, credentials, path, fileError));
out.replace(QLatin1Char('\n'), QString());
out.replace(QLatin1Char('\r'), QString());
if (fileError == ErrorCode::NoError && !out.isEmpty()) {
return ErrorCode::NoError;
}
if (attempt < 2) {
QThread::msleep(500);
}
}
logger.error() << "Xray readRealityKeyFiles: failed path=" << path;
return ErrorCode::XrayRealityKeysReadFailed;
};
ErrorCode errorCode = readKeyFile(QString::fromLatin1(amnezia::protocols::xray::PublicKeyPath), outPublicKey);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
return readKeyFile(QString::fromLatin1(amnezia::protocols::xray::shortidPath), outShortId);
}
ErrorCode XrayConfigurator::applyServerSettingsToRemote(const ServerCredentials &credentials, DockerContainer container,
ContainerConfig &containerConfig, const DnsSettings &dnsSettings,
bool appendNewClient, QString *outClientId)
{
ErrorCode errorCode = ErrorCode::NoError;
const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>();
if (!xrayCfg) {
logger.error() << "Xray applyServerSettings: missing XrayProtocolConfig";
return ErrorCode::InternalError;
}
const XrayServerConfig &srv = xrayCfg->serverConfig;
if (srv.isThirdPartyConfig) {
if (outClientId && xrayCfg->hasClientConfig()) {
*outClientId = xrayCfg->clientConfig->id;
}
return ErrorCode::NoError;
}
const QString flowValue = effectiveClientFlow(srv);
QString realityPublicKey;
QString realityShortId;
if (effectiveSecurity(srv) == QLatin1String("reality")) {
errorCode = readRealityKeyFiles(container, credentials, realityPublicKey, realityShortId);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Xray applyServerSettings: readRealityKeyFiles failed, error="
<< static_cast<int>(errorCode);
return errorCode;
}
}
QString currentConfig = m_sshSession->getTextFileFromContainer(
container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Xray applyServerSettings: getTextFileFromContainer failed, error="
<< static_cast<int>(errorCode) << "path=" << amnezia::protocols::xray::serverConfigPath;
return errorCode;
}
QJsonDocument doc = QJsonDocument::fromJson(currentConfig.toUtf8());
if (doc.isNull() || !doc.isObject()) {
logger.error() << "Failed to parse server config JSON";
return ErrorCode::XrayServerConfigInvalid;
}
QJsonObject serverConfig = doc.object();
const QJsonObject originalConfig = serverConfig;
QJsonArray clients = XrayServerConfig::clientsFromServerInboundJson(serverConfig);
QString clientId;
if (appendNewClient) {
const bool adoptingOwnAccount = !(xrayCfg->hasClientConfig() && !xrayCfg->clientConfig->id.isEmpty());
QString ownClientId;
if (readContainerKeyFile(container, credentials, QString::fromLatin1(amnezia::protocols::xray::uuidPath),
ownClientId)
!= ErrorCode::NoError) {
ownClientId.clear();
}
const int existingIndex =
adoptingOwnAccount ? XrayServerConfig::indexOfClient(clients, ownClientId) : -1;
if (existingIndex >= 0) {
clientId = ownClientId;
clients[existingIndex] =
XrayServerConfig::applyFlowToClient(clients[existingIndex].toObject(), flowValue);
logger.info() << "Xray applyServerSettings: reusing the account from the key file, clients="
<< clients.size();
} else {
const bool takeOverOwnAccount = adoptingOwnAccount && !ownClientId.isEmpty();
clientId = takeOverOwnAccount ? ownClientId : QUuid::createUuid().toString(QUuid::WithoutBraces);
clients.append(XrayServerConfig::makeClientEntry(clientId, flowValue));
logger.info() << "Xray applyServerSettings: added an account,"
<< (takeOverOwnAccount ? "taken over from the key file" : "freshly minted for a new user")
<< ", clients=" << clients.size();
}
} else {
if (clients.isEmpty()) {
logger.error() << "Server config has no VLESS clients";
return ErrorCode::XrayServerNoVlessClients;
}
clientId = XrayServerConfig::firstClientId(clients);
if (clientId.isEmpty()) {
logger.error() << "Server config VLESS client has empty id";
return ErrorCode::XrayServerNoVlessClients;
}
clients = XrayServerConfig::applyFlowToClients(clients, flowValue);
}
const XrayServerJsonStatus writeStatus =
XrayServerConfig::setClientsInServerInboundJson(serverConfig, clients);
if (writeStatus != XrayServerJsonStatus::Ok) {
logger.error() << "Xray applyServerSettings: server config has no place for the client list, status="
<< static_cast<int>(writeStatus);
return ErrorCode::XrayServerConfigInvalid;
}
const QString listenPort = srv.port.isEmpty() ? QString::fromLatin1(amnezia::protocols::xray::defaultPort)
: srv.port;
if (serverConfig == originalConfig) {
logger.info() << "Xray applyServerSettings: server config already matches, no rewrite and no restart,"
<< "clients=" << clients.size();
} else {
errorCode = uploadServerConfigAtomically(credentials, container, listenPort, serverConfig);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Xray applyServerSettings: apply failed, error=" << static_cast<int>(errorCode);
return errorCode;
}
}
if (outClientId) {
*outClientId = clientId;
}
XrayProtocolConfig updated =
buildClientProtocolConfig(credentials, container, srv, xrayCfg->clientTemplate, clientId, errorCode,
realityPublicKey, realityShortId);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Xray applyServerSettings: buildClientProtocolConfig failed, error="
<< static_cast<int>(errorCode);
return errorCode;
}
containerConfig.protocolConfig = updated;
return ErrorCode::NoError;
}
ErrorCode XrayConfigurator::readContainerKeyFile(DockerContainer container, const ServerCredentials &credentials,
const QString &path, QString &out) const
{
out.clear();
for (int attempt = 0; attempt < 3; ++attempt) {
ErrorCode fileError = ErrorCode::NoError;
out = QString::fromUtf8(m_sshSession->getTextFileFromContainer(container, credentials, path, fileError));
out.replace(QLatin1Char('\n'), QString());
out.replace(QLatin1Char('\r'), QString());
if (fileError == ErrorCode::NoError && !out.isEmpty()) {
return ErrorCode::NoError;
}
if (attempt < 2) {
QThread::msleep(500);
}
}
logger.error() << "Xray readContainerKeyFile: failed path=" << path;
return ErrorCode::XrayRealityKeysReadFailed;
}
ErrorCode XrayConfigurator::writeServerConfigForSetup(const ServerCredentials &credentials, DockerContainer container,
ContainerConfig &containerConfig, const DnsSettings &dnsSettings)
{
Q_UNUSED(dnsSettings);
namespace px = amnezia::protocols::xray;
const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>();
if (!xrayCfg) {
logger.error() << "Xray writeServerConfigForSetup: missing XrayProtocolConfig";
return ErrorCode::InternalError;
}
const XrayServerConfig &srv = xrayCfg->serverConfig;
if (srv.isThirdPartyConfig) {
return ErrorCode::NoError;
}
ErrorCode errorCode = ErrorCode::NoError;
QString clientId;
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::uuidPath), clientId);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Xray writeServerConfigForSetup: could not read the container uuid, error="
<< static_cast<int>(errorCode);
return errorCode;
}
const QString securityEff = effectiveSecurity(srv);
QString tlsPin;
if (securityEff == QLatin1String("tls")) {
errorCode = ensureTlsCertificate(credentials, container, tlsPin);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Xray writeServerConfigForSetup: TLS certificate is not on the server, error="
<< static_cast<int>(errorCode);
return errorCode;
}
}
QString realityPrivateKey;
QString realityPublicKey;
QString realityShortId;
if (securityEff == QLatin1String("reality")) {
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::PrivateKeyPath), realityPrivateKey);
if (errorCode != ErrorCode::NoError)
return errorCode;
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::PublicKeyPath), realityPublicKey);
if (errorCode != ErrorCode::NoError)
return errorCode;
errorCode = readContainerKeyFile(container, credentials, QString::fromLatin1(px::shortidPath), realityShortId);
if (errorCode != ErrorCode::NoError)
return errorCode;
}
const QString flowValue = effectiveClientFlow(srv);
const QJsonArray clients = collectServerClients(credentials, container, flowValue, clientId, errorCode);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Xray writeServerConfigForSetup: refusing to write over an unreadable client list";
return errorCode;
}
const QString portEff = srv.port.isEmpty() ? QString::fromLatin1(px::defaultPort) : srv.port;
if (securityEff != srv.security) {
logger.warning() << "Xray writeServerConfigForSetup: security downgraded for the server config, requested="
<< srv.security << "written=" << securityEff << "transport=" << srv.transport;
}
XrayServerInboundInputs inputs;
inputs.clients = clients;
inputs.realityPrivateKey = realityPrivateKey;
inputs.realityShortId = realityShortId;
const QJsonObject inboundJson = srv.toServerInboundJson(inputs);
const QString json = QString::fromUtf8(QJsonDocument(inboundJson).toJson());
errorCode = m_sshSession->uploadTextFileToContainer(container, credentials, json,
QString::fromLatin1(px::serverConfigPath),
libssh::ScpOverwriteMode::ScpOverwriteExisting);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Xray writeServerConfigForSetup: upload failed, error=" << static_cast<int>(errorCode)
<< "port=" << portEff;
return errorCode;
}
XrayProtocolConfig updated =
buildClientProtocolConfig(credentials, container, srv, xrayCfg->clientTemplate, clientId, errorCode,
realityPublicKey, realityShortId, tlsPin);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Xray writeServerConfigForSetup: buildClientProtocolConfig failed, error="
<< static_cast<int>(errorCode);
return errorCode;
}
updated.clientTemplate.updatedAt = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
// Written to the volume so a second administrator device can adopt the client-side
// settings. Failure is not fatal: the settings already apply on this device.
uploadClientTemplate(credentials, container, updated.clientTemplate);
containerConfig.protocolConfig = updated;
return ErrorCode::NoError;
}
QJsonArray XrayConfigurator::collectServerClients(const ServerCredentials &credentials, DockerContainer container,
const QString &flowValue, const QString &fallbackClientId,
ErrorCode &outError) const
{
namespace px = amnezia::protocols::xray;
outError = ErrorCode::NoError;
const QString configPath = QString::fromLatin1(px::serverConfigPath);
QString currentConfig;
for (int attempt = 0; attempt < 3; ++attempt) {
ErrorCode readError = ErrorCode::NoError;
currentConfig = m_sshSession->getTextFileFromContainer(container, credentials, configPath, readError);
if (readError == ErrorCode::NoError && !currentConfig.trimmed().isEmpty()) {
break;
}
currentConfig.clear();
if (attempt < 2) {
QThread::msleep(500);
}
}
QJsonArray existing;
if (currentConfig.trimmed().isEmpty()) {
QString probeOut;
auto collect = [&probeOut](const QString &data, libssh::Client &) {
probeOut += data + "\n";
return ErrorCode::NoError;
};
const QString probe =
QStringLiteral("if [ -s %1 ]; then echo amnezia_config=present; else echo amnezia_config=absent; fi")
.arg(configPath);
const ErrorCode probeError = m_sshSession->runContainerScript(credentials, container, probe, collect, collect);
if (probeError != ErrorCode::NoError || !probeOut.contains(QLatin1String("amnezia_config="))) {
logger.error() << "Xray collectServerClients: read outcome=unknown, cannot tell whether a server config "
"exists, probeError="
<< static_cast<int>(probeError);
outError = ErrorCode::XrayServerConfigInvalid;
return {};
}
if (probeOut.contains(QLatin1String("amnezia_config=present"))) {
logger.error() << "Xray collectServerClients: read outcome=unreadable, server config exists but could not "
"be read, aborting so the existing client list is not lost";
outError = ErrorCode::XrayServerConfigInvalid;
return {};
}
} else {
const QJsonDocument doc = QJsonDocument::fromJson(currentConfig.toUtf8());
if (!doc.isObject()) {
logger.error() << "Xray collectServerClients: read outcome=unparseable, server config is not valid JSON, "
"bytes="
<< currentConfig.size();
outError = ErrorCode::XrayServerConfigInvalid;
return {};
}
existing = XrayServerConfig::clientsFromServerInboundJson(doc.object());
}
QJsonArray clients =
XrayServerConfig::applyFlowToClients(existing, flowValue, XrayClientListFilter::DropWithoutId);
if (!fallbackClientId.isEmpty()
&& XrayServerConfig::indexOfClient(clients, fallbackClientId) < 0) {
clients.append(XrayServerConfig::makeClientEntry(fallbackClientId, flowValue));
}
return clients;
}
bool XrayConfigurator::isSecuritySupportedOnSelfHosted(const XrayServerConfig &srv)
{
Q_UNUSED(srv);
// TLS is issued in writeServerConfigForSetup (self-signed pems in the volume).
return true;
}
ErrorCode XrayConfigurator::ensureTlsCertificate(const ServerCredentials &credentials, DockerContainer container,
QString &outFingerprint) const
{
namespace px = amnezia::protocols::xray;
outFingerprint.clear();
QString stdOut;
auto collect = [&stdOut](const QString &data, libssh::Client &) {
stdOut += data + QLatin1Char('\n');
return ErrorCode::NoError;
};
const QString certPath = QString::fromLatin1(px::tlsCertPath);
const QString keyPath = QString::fromLatin1(px::tlsKeyPath);
const QString script = QStringLiteral(
"has_key() { [ -f \"$1\" ] && [ -n \"$(tr -d '[:space:]' < \"$1\" 2>/dev/null)\" ]; }\n"
"CERT=%1\n"
"KEY=%2\n"
"if has_key \"$CERT\" && has_key \"$KEY\"; then echo amnezia_tls=present;\n"
"else\n"
" openssl req -x509 -newkey rsa:2048 -nodes -days 3650 -subj \"/CN=amnezia-xray\" "
"-keyout \"$KEY\" -out \"$CERT\" || true\n"
" if has_key \"$CERT\" && has_key \"$KEY\"; then echo amnezia_tls=created;\n"
" else echo amnezia_tls=failed; exit 1; fi\n"
"fi\n"
"FP=$(openssl x509 -noout -fingerprint -sha256 -in \"$CERT\" 2>/dev/null "
"| sed 's/.*Fingerprint=//' | tr -d ':' | tr 'A-F' 'a-f')\n"
"if [ -z \"$FP\" ]; then echo amnezia_tls=failed; exit 1; fi\n"
"echo amnezia_tls_fp=$FP\n"
"exit 0\n")
.arg(certPath, keyPath);
const ErrorCode scriptError = m_sshSession->runContainerScript(credentials, container, script, collect, collect);
const bool present = stdOut.contains(QLatin1String("amnezia_tls=present"));
const bool created = stdOut.contains(QLatin1String("amnezia_tls=created"));
outFingerprint = tlsFingerprintFromEnsureOutput(stdOut);
if (scriptError != ErrorCode::NoError || (!present && !created) || outFingerprint.isEmpty()) {
logger.error() << "Xray ensureTlsCertificate: openssl failed, error=" << static_cast<int>(scriptError)
<< "out=" << stdOut.trimmed();
outFingerprint.clear();
return ErrorCode::XrayTlsNotSupported;
}
if (created) {
logger.info() << "Xray ensureTlsCertificate: issued a self-signed certificate into the volume";
} else {
logger.info() << "Xray ensureTlsCertificate: using the existing certificate in the volume";
}
return ErrorCode::NoError;
}
ErrorCode XrayConfigurator::uploadServerConfigAtomically(const ServerCredentials &credentials,
DockerContainer container, const QString &listenPort,
const QJsonObject &serverConfig) const
{
namespace px = amnezia::protocols::xray;
const QString livePath = QString::fromLatin1(px::serverConfigPath);
const QString stagedPath = QStringLiteral("/opt/amnezia/xray/server.new.json");
const QString json = QString::fromUtf8(QJsonDocument(serverConfig).toJson());
ErrorCode errorCode = m_sshSession->uploadTextFileToContainer(container, credentials, json, stagedPath,
libssh::ScpOverwriteMode::ScpOverwriteExisting);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Xray atomic apply: failed to stage config, error=" << static_cast<int>(errorCode);
return errorCode;
}
const QString script = QStringLiteral(
"LIVE=%1\n"
"STAGED=%2\n"
"PORT=%3\n"
"BACKUP=\"$LIVE.bak\"\n"
"if [ ! -s \"$STAGED\" ]; then echo \"amnezia_apply=missing\"; exit 0; fi\n"
"if ! xray -test -format json -config \"$STAGED\" > /tmp/xray_test.log 2>&1; then\n"
" echo \"amnezia_apply=invalid\"\n"
" cat /tmp/xray_test.log\n"
" rm -f \"$STAGED\"\n"
" exit 0\n"
"fi\n"
"serving() {\n"
" netstat -lntup 2>/dev/null | grep -E \"[:.]$PORT[[:space:]]\" | grep -q xray\n"
"}\n"
"if [ -f \"$LIVE\" ]; then cp \"$LIVE\" \"$BACKUP\"; fi\n"
"mv \"$STAGED\" \"$LIVE\"\n"
"killall -KILL xray 2>/dev/null\n"
"sleep 1\n"
"(xray -config \"$LIVE\" > /dev/null 2>&1 &)\n"
"sleep 3\n"
"if serving; then\n"
" rm -f \"$BACKUP\"\n"
" echo \"amnezia_apply=applied\"\n"
" exit 0\n"
"fi\n"
"if [ -f \"$BACKUP\" ]; then\n"
" cp \"$BACKUP\" \"$LIVE\"\n"
" killall -KILL xray 2>/dev/null\n"
" sleep 1\n"
" (xray -config \"$LIVE\" > /dev/null 2>&1 &)\n"
" sleep 3\n"
" if serving; then echo \"amnezia_apply=rolled_back\"; else echo \"amnezia_apply=rollback_failed\"; fi\n"
" exit 0\n"
"fi\n"
"echo \"amnezia_apply=rollback_failed\"\n")
.arg(livePath, stagedPath, listenPort);
QString stdOut;
auto collect = [&stdOut](const QString &data, libssh::Client &) {
stdOut += data + "\n";
return ErrorCode::NoError;
};
errorCode = m_sshSession->runContainerScript(credentials, container, script, collect, collect);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Xray atomic apply: script failed, error=" << static_cast<int>(errorCode);
return errorCode;
}
if (stdOut.contains(QLatin1String("amnezia_apply=applied"))) {
logger.info() << "Xray atomic apply: outcome=applied, new config is live and serving on port=" << listenPort;
return ErrorCode::NoError;
}
if (stdOut.contains(QLatin1String("amnezia_apply=missing"))) {
logger.error() << "Xray atomic apply: outcome=staged-file-missing, staged config was not on the server, "
"live config untouched, port="
<< listenPort;
return ErrorCode::XrayServerConfigRejected;
}
if (stdOut.contains(QLatin1String("amnezia_apply=invalid"))) {
logger.error() << "Xray atomic apply: outcome=rejected, xray -test refused the config, live config untouched, "
"port="
<< listenPort << "testOutputBytes=" << stdOut.size() << "(output withheld, may contain keys)";
return ErrorCode::XrayServerConfigRejected;
}
if (stdOut.contains(QLatin1String("amnezia_apply=rolled_back"))) {
logger.error() << "Xray atomic apply: outcome=rolled-back, new config did not serve on port=" << listenPort
<< ", previous config restored and serving";
return ErrorCode::XrayServerConfigRolledBack;
}
if (stdOut.contains(QLatin1String("amnezia_apply=rollback_failed"))) {
logger.error() << "Xray atomic apply: outcome=ROLLBACK-FAILED, nothing is serving on port=" << listenPort
<< ", the xray container is down and the previous config could not be brought back";
return ErrorCode::XrayServerNotServing;
}
logger.error() << "Xray atomic apply: outcome=unrecognised, no marker in script output, port=" << listenPort
<< "outputBytes=" << stdOut.size() << "output="
<< stdOut.simplified().left(200);
return ErrorCode::XrayServerConfigInvalid;
}
QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container,
const ContainerConfig &containerConfig,
const DnsSettings &dnsSettings,
ErrorCode &errorCode)
{
ContainerConfig mutableConfig = containerConfig;
QString clientId;
const ErrorCode applyError =
applyServerSettingsToRemote(credentials, container, mutableConfig, dnsSettings, true, &clientId);
errorCode = applyError;
if (applyError != ErrorCode::NoError || clientId.isEmpty()) {
return QString();
// Generate new UUID for client
QString clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);
// Get current server config
QString currentConfig = m_sshSession->getTextFileFromContainer(
container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to get server config file";
return "";
}
// Parse current config as JSON
QJsonDocument doc = QJsonDocument::fromJson(currentConfig.toUtf8());
if (doc.isNull() || !doc.isObject()) {
logger.error() << "Failed to parse server config JSON";
errorCode = ErrorCode::InternalError;
return "";
}
QJsonObject serverConfig = doc.object();
// Validate server config structure
if (!serverConfig.contains(amnezia::protocols::xray::inbounds)) {
logger.error() << "Server config missing 'inbounds' field";
errorCode = ErrorCode::InternalError;
return "";
}
QJsonArray inbounds = serverConfig[amnezia::protocols::xray::inbounds].toArray();
if (inbounds.isEmpty()) {
logger.error() << "Server config has empty 'inbounds' array";
errorCode = ErrorCode::InternalError;
return "";
}
QJsonObject inbound = inbounds[0].toObject();
if (!inbound.contains(amnezia::protocols::xray::settings)) {
logger.error() << "Inbound missing 'settings' field";
errorCode = ErrorCode::InternalError;
return "";
}
QJsonObject settings = inbound[amnezia::protocols::xray::settings].toObject();
if (!settings.contains(amnezia::protocols::xray::clients)) {
logger.error() << "Settings missing 'clients' field";
errorCode = ErrorCode::InternalError;
return "";
}
QJsonArray clients = settings[amnezia::protocols::xray::clients].toArray();
// Create configuration for new client
QJsonObject clientConfig {
{amnezia::protocols::xray::id, clientId},
{amnezia::protocols::xray::flow, "xtls-rprx-vision"}
};
clients.append(clientConfig);
// Update config
settings[amnezia::protocols::xray::clients] = clients;
inbound[amnezia::protocols::xray::settings] = settings;
inbounds[0] = inbound;
serverConfig[amnezia::protocols::xray::inbounds] = inbounds;
// Save updated config to server
QString updatedConfig = QJsonDocument(serverConfig).toJson();
errorCode = m_sshSession->uploadTextFileToContainer(
container,
credentials,
updatedConfig,
amnezia::protocols::xray::serverConfigPath,
libssh::ScpOverwriteMode::ScpOverwriteExisting
);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to upload updated config";
return "";
}
// Restart container
QString restartScript = QString("sudo docker restart $CONTAINER_NAME");
errorCode = m_sshSession->runScript(
credentials,
m_sshSession->replaceVars(restartScript, amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns, dnsSettings.secondaryDns))
);
if (errorCode != ErrorCode::NoError) {
logger.error() << "Failed to restart container";
return "";
}
return clientId;
}
bool XrayConfigurator::uploadClientTemplate(const ServerCredentials &credentials, DockerContainer container,
const XrayClientTemplate &clientTemplate) const
{
namespace px = amnezia::protocols::xray;
XrayClientTemplate stamped = clientTemplate;
stamped.updatedAt = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
const QJsonObject templateJson = stamped.toJson();
const QString json = QString::fromUtf8(QJsonDocument(templateJson).toJson());
const ErrorCode errorCode = m_sshSession->uploadTextFileToContainer(
container, credentials, json, QString::fromLatin1(px::clientTemplatePath),
libssh::ScpOverwriteMode::ScpOverwriteExisting);
if (errorCode != ErrorCode::NoError) {
logger.warning() << "Xray uploadClientTemplate: failed, error=" << static_cast<int>(errorCode)
<< ", this device is now ahead of the template stored on the server";
return false;
}
return true;
}
XrayClientTemplate XrayConfigurator::readClientTemplate(const ServerCredentials &credentials,
DockerContainer container, bool &outFound) const
{
namespace px = amnezia::protocols::xray;
outFound = false;
ErrorCode readError = ErrorCode::NoError;
const QString content = QString::fromUtf8(m_sshSession->getTextFileFromContainer(
container, credentials, QString::fromLatin1(px::clientTemplatePath), readError));
if (readError != ErrorCode::NoError || content.trimmed().isEmpty()) {
return {};
}
const QJsonDocument doc = QJsonDocument::fromJson(content.toUtf8());
if (!doc.isObject()) {
logger.warning() << "Xray readClientTemplate: template on the server is not valid json";
return {};
}
XrayClientTemplate tpl = XrayClientTemplate::fromJson(doc.object());
if (tpl.formatVersion == 0) {
logger.warning() << "Xray readClientTemplate: template on the server has no format version, ignoring it";
return {};
}
outFound = true;
return tpl;
}
XrayProtocolConfig XrayConfigurator::buildClientProtocolConfig(const ServerCredentials &credentials,
DockerContainer container,
const XrayServerConfig &srv,
const XrayClientTemplate &tpl, const QString &clientId,
ErrorCode &errorCode,
const QString &prefetchedRealityPublicKey,
const QString &prefetchedRealityShortId,
const QString &prefetchedTlsPin) const
{
namespace px = amnezia::protocols::xray;
QString xrayPublicKey = prefetchedRealityPublicKey;
QString xrayShortId = prefetchedRealityShortId;
QString tlsPin = prefetchedTlsPin;
const QString securityEff = effectiveSecurity(srv);
if (securityEff == QLatin1String(px::securityReality)) {
if (xrayPublicKey.isEmpty() || xrayShortId.isEmpty()) {
errorCode = readRealityKeyFiles(container, credentials, xrayPublicKey, xrayShortId);
if (errorCode != ErrorCode::NoError) {
return {};
}
}
}
if (securityEff == QLatin1String(px::securityTls) && !srv.isThirdPartyConfig) {
if (tlsPin.isEmpty()) {
errorCode = ensureTlsCertificate(credentials, container, tlsPin);
if (errorCode != ErrorCode::NoError) {
return {};
}
}
if (tlsPin.isEmpty()) {
logger.error() << "Xray buildClientProtocolConfig: TLS pin is empty after ensure";
errorCode = ErrorCode::XrayTlsNotSupported;
return {};
}
}
if (securityEff != srv.security) {
logger.warning() << "Xray buildClientProtocolConfig: security downgraded for the client config, requested="
<< srv.security << "written=" << securityEff << "transport=" << srv.transport;
}
XrayProtocolConfig protocolConfig;
protocolConfig.serverConfig = srv;
protocolConfig.clientTemplate = tpl;
XrayClientOutboundInputs inputs;
inputs.serverAddress = credentials.hostName;
inputs.clientId = clientId;
inputs.realityPublicKey = xrayPublicKey;
inputs.realityShortId = xrayShortId;
inputs.tlsPinnedPeerCertSha256 = tlsPin;
XrayClientConfig clientConfig;
clientConfig.nativeConfig = QString::fromUtf8(
QJsonDocument(protocolConfig.toClientOutboundJson(inputs)).toJson(QJsonDocument::Compact));
clientConfig.localPort = QString::fromLatin1(px::defaultLocalProxyPort);
clientConfig.id = clientId;
clientConfig.templateFingerprint = tpl.contentFingerprint();
protocolConfig.setClientConfig(clientConfig);
return protocolConfig;
}
ProtocolConfig XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container,
const ContainerConfig &containerConfig,
const DnsSettings &dnsSettings,
ErrorCode &errorCode)
const ContainerConfig &containerConfig,
const DnsSettings &dnsSettings,
ErrorCode &errorCode)
{
if (const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
if (xrayCfg->serverConfig.isThirdPartyConfig && xrayCfg->hasClientConfig()) {
logger.info() << "Xray createConfig: returning existing third-party client config without server SSH";
return *xrayCfg;
}
const XrayServerConfig* serverConfig = nullptr;
if (auto* xrayConfig = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
serverConfig = &xrayConfig->serverConfig;
}
const XrayServerConfig *serverConfig = nullptr;
XrayClientTemplate clientTemplate;
if (const auto *xrayCfg = containerConfig.protocolConfig.as<XrayProtocolConfig>()) {
serverConfig = &xrayCfg->serverConfig;
clientTemplate = xrayCfg->clientTemplate;
}
if (!serverConfig) {
logger.error() << "No XrayProtocolConfig found";
QString xrayClientId = prepareServerConfig(credentials, container, containerConfig, dnsSettings, errorCode);
if (errorCode != ErrorCode::NoError || xrayClientId.isEmpty()) {
logger.error() << "Failed to prepare server config";
errorCode = ErrorCode::InternalError;
return XrayProtocolConfig{};
}
const XrayServerConfig &srv = *serverConfig;
QString xrayClientId = prepareServerConfig(credentials, container, containerConfig, dnsSettings, errorCode);
if (errorCode != ErrorCode::NoError || xrayClientId.isEmpty()) {
logger.error() << "Failed to prepare server config";
if (errorCode == ErrorCode::NoError) {
errorCode = ErrorCode::InternalError;
}
amnezia::ScriptVars vars = amnezia::genBaseVars(credentials, container, dnsSettings.primaryDns, dnsSettings.secondaryDns);
vars.append(amnezia::genProtocolVarsForContainer(container, containerConfig));
QString config = m_sshSession->replaceVars(amnezia::scriptData(ProtocolScriptType::xray_template, container), vars);
if (config.isEmpty()) {
logger.error() << "Failed to get config template";
errorCode = ErrorCode::InternalError;
return XrayProtocolConfig{};
}
return buildClientProtocolConfig(credentials, container, srv, clientTemplate, xrayClientId, errorCode);
}
QString xrayPublicKey =
m_sshSession->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::PublicKeyPath, errorCode);
if (errorCode != ErrorCode::NoError || xrayPublicKey.isEmpty()) {
logger.error() << "Failed to get public key";
errorCode = ErrorCode::InternalError;
return XrayProtocolConfig{};
}
xrayPublicKey.replace("\n", "");
QString xrayShortId =
m_sshSession->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::shortidPath, errorCode);
if (errorCode != ErrorCode::NoError || xrayShortId.isEmpty()) {
logger.error() << "Failed to get short ID";
errorCode = ErrorCode::InternalError;
return XrayProtocolConfig{};
}
xrayShortId.replace("\n", "");
if (!config.contains("$XRAY_CLIENT_ID") || !config.contains("$XRAY_PUBLIC_KEY") || !config.contains("$XRAY_SHORT_ID")) {
logger.error() << "Config template missing required variables:"
<< "XRAY_CLIENT_ID:" << !config.contains("$XRAY_CLIENT_ID")
<< "XRAY_PUBLIC_KEY:" << !config.contains("$XRAY_PUBLIC_KEY")
<< "XRAY_SHORT_ID:" << !config.contains("$XRAY_SHORT_ID");
errorCode = ErrorCode::InternalError;
return XrayProtocolConfig{};
}
config.replace("$XRAY_CLIENT_ID", xrayClientId);
config.replace("$XRAY_PUBLIC_KEY", xrayPublicKey);
config.replace("$XRAY_SHORT_ID", xrayShortId);
XrayProtocolConfig protocolConfig;
if (serverConfig) {
protocolConfig.serverConfig = *serverConfig;
}
XrayClientConfig clientConfig;
clientConfig.nativeConfig = config;
clientConfig.localPort = "";
clientConfig.id = xrayClientId;
protocolConfig.setClientConfig(clientConfig);
return protocolConfig;
}

View File

@@ -2,14 +2,11 @@
#define XRAY_CONFIGURATOR_H
#include <QObject>
#include <QJsonArray>
#include <QJsonObject>
#include "configuratorBase.h"
#include "core/utils/errorCodes.h"
#include "core/utils/routeModes.h"
#include "core/utils/commonStructs.h"
#include "core/models/protocols/xrayProtocolConfig.h"
class XrayConfigurator : public ConfiguratorBase
{
@@ -21,67 +18,10 @@ public:
const amnezia::DnsSettings &dnsSettings,
amnezia::ErrorCode &errorCode) override;
amnezia::ProtocolConfig processConfigWithLocalSettings(const amnezia::ConnectionSettings &settings,
amnezia::ProtocolConfig protocolConfig) override;
amnezia::ErrorCode applyServerSettingsToRemote(const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container,
amnezia::ContainerConfig &containerConfig,
const amnezia::DnsSettings &dnsSettings,
bool appendNewClient,
QString *outClientId = nullptr);
amnezia::ErrorCode writeServerConfigForSetup(const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container,
amnezia::ContainerConfig &containerConfig,
const amnezia::DnsSettings &dnsSettings);
bool uploadClientTemplate(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container,
const amnezia::XrayClientTemplate &clientTemplate) const;
amnezia::XrayClientTemplate readClientTemplate(const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container, bool &outFound) const;
static bool isSecuritySupportedOnSelfHosted(const amnezia::XrayServerConfig &srv);
private:
QJsonArray collectServerClients(const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container, const QString &flowValue,
const QString &fallbackClientId, amnezia::ErrorCode &outError) const;
amnezia::ErrorCode uploadServerConfigAtomically(const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container, const QString &listenPort,
const QJsonObject &serverConfig) const;
amnezia::ErrorCode readContainerKeyFile(amnezia::DockerContainer container,
const amnezia::ServerCredentials &credentials,
const QString &path, QString &out) const;
QString prepareServerConfig(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container, const amnezia::ContainerConfig &containerConfig,
const amnezia::DnsSettings &dnsSettings,
amnezia::ErrorCode &errorCode);
amnezia::ErrorCode uploadServerConfigJson(const amnezia::ServerCredentials &credentials, amnezia::DockerContainer container,
const amnezia::DnsSettings &dnsSettings, const QJsonObject &serverConfig) const;
amnezia::XrayProtocolConfig buildClientProtocolConfig(const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container,
const amnezia::XrayServerConfig &srv,
const amnezia::XrayClientTemplate &tpl,
const QString &clientId,
amnezia::ErrorCode &errorCode,
const QString &prefetchedRealityPublicKey = {},
const QString &prefetchedRealityShortId = {},
const QString &prefetchedTlsPin = {}) const;
amnezia::ErrorCode readRealityKeyFiles(amnezia::DockerContainer container,
const amnezia::ServerCredentials &credentials,
QString &outPublicKey,
QString &outShortId) const;
amnezia::ErrorCode ensureTlsCertificate(const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container,
QString &outFingerprint) const;
};
#endif // XRAY_CONFIGURATOR_H

View File

@@ -2,8 +2,6 @@
#include "core/controllers/gatewayController.h"
#include "core/repositories/secureServersRepository.h"
#include "core/utils/api/apiUtils.h"
#include "core/utils/api/gatewayPayloadBuilder.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/constants/apiConstants.h"
#include <QtConcurrent/QtConcurrent>
@@ -80,15 +78,17 @@ QFuture<QPair<ErrorCode, QJsonArray>> NewsController::fetchNews()
m_appSettingsRepository->getGatewayEndpoint(),
m_appSettingsRepository->isDevGatewayEnv(),
apiDefs::requestTimeoutMsecs,
m_appSettingsRepository->isStrictKillSwitchEnabled(),
m_appSettingsRepository);
m_appSettingsRepository->isStrictKillSwitchEnabled());
// both country codes and service types are arrays here, one entry per gateway stack the user has
const QJsonObject payload = GatewayPayloadBuilder(m_appSettingsRepository)
.addField(apiDefs::key::locale, apiUtils::getAppLanguageCode(m_appSettingsRepository))
.addField(apiDefs::key::userCountryCode, services.value(apiDefs::key::userCountryCode))
.addField(apiDefs::key::serviceType, services.value(apiDefs::key::serviceType))
.build();
QJsonObject payload;
payload.insert("locale", m_appSettingsRepository->getAppLanguage().name().split("_").first());
if (services.contains(apiDefs::key::userCountryCode)) {
payload.insert(apiDefs::key::userCountryCode, services.value(apiDefs::key::userCountryCode));
}
if (services.contains(apiDefs::key::serviceType)) {
payload.insert(apiDefs::key::serviceType, services.value(apiDefs::key::serviceType));
}
auto future = gatewayController->postAsync(QString("%1v1/news"), payload);
return future.then([gatewayController](QPair<ErrorCode, QByteArray> result) -> QPair<ErrorCode, QJsonArray> {

View File

@@ -1,6 +1,7 @@
#include "servicesCatalogController.h"
#include <QJsonDocument>
#include <QSysInfo>
#include <QJsonArray>
#include <QEventLoop>
#include <QDebug>
@@ -10,7 +11,6 @@
#include <limits>
#include "core/controllers/gatewayController.h"
#include "core/utils/api/gatewayPayloadBuilder.h"
#include "core/utils/serverConfigUtils.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/constants/apiConstants.h"
@@ -212,7 +212,11 @@ ServicesCatalogController::ServicesCatalogController(SecureAppSettingsRepository
ErrorCode ServicesCatalogController::fillAvailableServices(QJsonObject &servicesData)
{
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository).build();
QJsonObject apiPayload;
apiPayload[apiDefs::key::osVersion] = QSysInfo::productType();
apiPayload[apiDefs::key::appVersion] = QString(APP_VERSION);
apiPayload[apiDefs::key::cliName] = QString(APPLICATION_NAME);
apiPayload[apiDefs::key::appLanguage] = m_appSettingsRepository->getAppLanguage().name().split("_").first();
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/services"), apiPayload, responseBody);
@@ -238,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);
m_appSettingsRepository->isStrictKillSwitchEnabled());
return gatewayController.post(endpoint, apiPayload, responseBody);
}

View File

@@ -5,9 +5,9 @@
#include <QEventLoop>
#include <QFutureWatcher>
#include <QJsonDocument>
#include <QJsonObject>
#include <QPromise>
#include <QSet>
#include <QSysInfo>
#include <QUuid>
#include <QVariantMap>
@@ -20,7 +20,6 @@
#include "core/utils/constants/apiKeys.h"
#include "core/utils/constants/apiConstants.h"
#include "core/utils/api/apiUtils.h"
#include "core/utils/api/gatewayPayloadBuilder.h"
#include "core/controllers/gatewayController.h"
#include "core/utils/protocolEnum.h"
#include "core/protocols/protocolUtils.h"
@@ -32,7 +31,7 @@
#if defined(Q_OS_IOS) || defined(MACOS_NE)
#include "platforms/ios/ios_controller.h"
#include "core/utils/swiftBridge.h"
#include <AmneziaVPN-Swift.h>
#endif
using namespace amnezia;
@@ -56,38 +55,6 @@ 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;
}
}
@@ -97,6 +64,39 @@ SubscriptionController::SubscriptionController(SecureServersRepository* serversR
{
}
QJsonObject SubscriptionController::GatewayRequestData::toJsonObject() const
{
QJsonObject obj;
if (!osVersion.isEmpty()) {
obj[apiDefs::key::osVersion] = osVersion;
}
if (!appVersion.isEmpty()) {
obj[apiDefs::key::appVersion] = appVersion;
}
if (!appLanguage.isEmpty()) {
obj[apiDefs::key::appLanguage] = appLanguage;
}
if (!installationUuid.isEmpty()) {
obj[apiDefs::key::uuid] = installationUuid;
}
if (!userCountryCode.isEmpty()) {
obj[apiDefs::key::userCountryCode] = userCountryCode;
}
if (!serverCountryCode.isEmpty()) {
obj[apiDefs::key::serverCountryCode] = serverCountryCode;
}
if (!serviceType.isEmpty()) {
obj[apiDefs::key::serviceType] = serviceType;
}
if (!serviceProtocol.isEmpty()) {
obj[apiDefs::key::serviceProtocol] = serviceProtocol;
}
if (!authData.isEmpty()) {
obj[apiDefs::key::authData] = authData;
}
return obj;
}
SubscriptionController::ProtocolData SubscriptionController::generateProtocolData(const QString &protocol)
{
ProtocolData protocolData;
@@ -111,15 +111,13 @@ SubscriptionController::ProtocolData SubscriptionController::generateProtocolDat
return protocolData;
}
QString SubscriptionController::publicKeyForProtocol(const QString &protocol, const ProtocolData &protocolData)
void SubscriptionController::appendProtocolDataToApiPayload(const QString &protocol, const ProtocolData &protocolData, QJsonObject &apiPayload)
{
if (protocol == configKey::awg) {
return protocolData.wireGuardClientPubKey;
apiPayload[apiDefs::key::publicKey] = protocolData.wireGuardClientPubKey;
} else if (protocol == configKey::vless) {
apiPayload[apiDefs::key::publicKey] = protocolData.xrayUuid;
}
if (protocol == configKey::vless) {
return protocolData.xrayUuid;
}
return {};
}
ErrorCode SubscriptionController::extractServerConfigJsonFromResponse(const QByteArray &apiResponseBody, const QString &protocol,
@@ -157,14 +155,23 @@ ErrorCode SubscriptionController::extractServerConfigJsonFromResponse(const QByt
// TODO looks like this block can be removed after v1 configs EOL
const QStringList awgProtocolKeys = configKey::awgProtocolKeys();
serverProtocolConfig[configKey::junkPacketCount] = clientProtocolConfig.value(configKey::junkPacketCount);
serverProtocolConfig[configKey::junkPacketMinSize] = clientProtocolConfig.value(configKey::junkPacketMinSize);
serverProtocolConfig[configKey::junkPacketMaxSize] = clientProtocolConfig.value(configKey::junkPacketMaxSize);
serverProtocolConfig[configKey::initPacketJunkSize] = clientProtocolConfig.value(configKey::initPacketJunkSize);
serverProtocolConfig[configKey::responsePacketJunkSize] = clientProtocolConfig.value(configKey::responsePacketJunkSize);
serverProtocolConfig[configKey::initPacketMagicHeader] = clientProtocolConfig.value(configKey::initPacketMagicHeader);
serverProtocolConfig[configKey::responsePacketMagicHeader] = clientProtocolConfig.value(configKey::responsePacketMagicHeader);
serverProtocolConfig[configKey::underloadPacketMagicHeader] = clientProtocolConfig.value(configKey::underloadPacketMagicHeader);
serverProtocolConfig[configKey::transportPacketMagicHeader] = clientProtocolConfig.value(configKey::transportPacketMagicHeader);
for (const QString &key : awgProtocolKeys) {
const QJsonValue value = clientProtocolConfig.value(key);
if (value.isString() && !value.toString().isEmpty()) {
serverProtocolConfig[key] = value;
}
}
serverProtocolConfig[configKey::cookieReplyPacketJunkSize] = clientProtocolConfig.value(configKey::cookieReplyPacketJunkSize);
serverProtocolConfig[configKey::transportPacketJunkSize] = clientProtocolConfig.value(configKey::transportPacketJunkSize);
serverProtocolConfig[configKey::specialJunk1] = clientProtocolConfig.value(configKey::specialJunk1);
serverProtocolConfig[configKey::specialJunk2] = clientProtocolConfig.value(configKey::specialJunk2);
serverProtocolConfig[configKey::specialJunk3] = clientProtocolConfig.value(configKey::specialJunk3);
serverProtocolConfig[configKey::specialJunk4] = clientProtocolConfig.value(configKey::specialJunk4);
serverProtocolConfig[configKey::specialJunk5] = clientProtocolConfig.value(configKey::specialJunk5);
//
@@ -190,6 +197,9 @@ 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());
}
@@ -201,29 +211,28 @@ 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);
m_appSettingsRepository->isStrictKillSwitchEnabled());
return gatewayController.post(endpoint, apiPayload, responseBody);
}
ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const ProtocolData &protocolData,
CaptchaInfo &captchaInfo)
const QString &serviceProtocol, const ProtocolData &protocolData)
{
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
.addField(apiDefs::key::userCountryCode, userCountryCode)
.addField(apiDefs::key::serviceType, serviceType)
.addField(apiDefs::key::serviceProtocol, serviceProtocol)
.addField(apiDefs::key::publicKey, publicKeyForProtocol(serviceProtocol, protocolData))
.build();
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_appSettingsRepository->getAppLanguage().name().split("_").first(),
m_appSettingsRepository->getInstallationUuid(true),
userCountryCode,
"",
serviceType,
serviceProtocol,
QJsonObject() };
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
appendProtocolDataToApiPayload(serviceProtocol, protocolData, apiPayload);
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody);
if (errorCode == ErrorCode::ApiCaptchaRequiredError) {
fillCaptchaInfoFromResponse(responseBody, captchaInfo);
return errorCode;
}
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
@@ -233,9 +242,9 @@ ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCo
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
updateApiConfigInJson(serverConfigJson, serviceType, serviceProtocol, userCountryCode, responseBody);
if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
return ErrorCode::InternalError;
}
@@ -254,15 +263,20 @@ ErrorCode SubscriptionController::importTrialFromGateway(const QString &userCoun
return ErrorCode::ApiConfigEmptyError;
}
ProtocolData protocolData = generateProtocolData(serviceProtocol);
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_appSettingsRepository->getAppLanguage().name().split("_").first(),
m_appSettingsRepository->getInstallationUuid(true),
userCountryCode,
"",
serviceType,
serviceProtocol,
QJsonObject() };
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
.addField(apiDefs::key::userCountryCode, userCountryCode)
.addField(apiDefs::key::serviceType, serviceType)
.addField(apiDefs::key::serviceProtocol, serviceProtocol)
.addField(apiDefs::key::publicKey, publicKeyForProtocol(serviceProtocol, protocolData))
.addField(apiDefs::key::email, trimmedEmail)
.build();
ProtocolData protocolData = generateProtocolData(serviceProtocol);
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
appendProtocolDataToApiPayload(serviceProtocol, protocolData, apiPayload);
apiPayload.insert(apiDefs::key::email, trimmedEmail);
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/trial"), apiPayload, responseBody);
@@ -303,13 +317,19 @@ ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userC
const QString &transactionId, bool isTestPurchase,
int *duplicateServerIndex)
{
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
.addField(apiDefs::key::userCountryCode, userCountryCode)
.addField(apiDefs::key::serviceType, serviceType)
.addField(apiDefs::key::serviceProtocol, serviceProtocol)
.addField(apiDefs::key::publicKey, publicKeyForProtocol(serviceProtocol, protocolData))
.addField(apiDefs::key::transactionId, transactionId)
.build();
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_appSettingsRepository->getAppLanguage().name().split("_").first(),
m_appSettingsRepository->getInstallationUuid(true),
userCountryCode,
"",
serviceType,
serviceProtocol,
QJsonObject() };
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
appendProtocolDataToApiPayload(serviceProtocol, protocolData, apiPayload);
apiPayload[apiDefs::key::transactionId] = transactionId;
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody, isTestPurchase);
@@ -364,6 +384,8 @@ ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userC
ApiV2ServerConfig apiV2ServerConfig = ApiV2ServerConfig::fromJson(configObject);
ApiV2ServerConfig* apiV2 = &apiV2ServerConfig;
apiV2->apiConfig.vpnKey = normalizedKey;
apiV2->apiConfig.isTestPurchase = isTestPurchase;
apiV2->apiConfig.isInAppPurchase = true;
apiV2->apiConfig.subscriptionExpiredByServer = false;
apiV2->crc = crc;
@@ -373,8 +395,7 @@ ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userC
return ErrorCode::NoError;
}
ErrorCode SubscriptionController::updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent,
CaptchaInfo *captchaInfoOut, ProtocolData *usedProtocolDataOut)
ErrorCode SubscriptionController::updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent)
{
auto apiV2 = m_serversRepository->apiV2Config(serverId);
if (!apiV2.has_value()) {
@@ -382,43 +403,29 @@ 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),
m_appSettingsRepository->getAppLanguage().name().split("_").first(),
m_appSettingsRepository->getInstallationUuid(true),
apiV2->apiConfig.userCountryCode,
newCountryCode,
apiV2->serviceType(),
serviceProtocol,
authDataJson };
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
.addField(apiDefs::key::userCountryCode, apiV2->apiConfig.userCountryCode)
.addField(apiDefs::key::serverCountryCode, newCountryCode)
.addField(apiDefs::key::serviceType, apiV2->serviceType())
.addField(apiDefs::key::serviceProtocol, serviceProtocol)
.addField(apiDefs::key::publicKey, publicKeyForProtocol(serviceProtocol, protocolData))
.addField(apiDefs::key::authData, apiV2->authData.toJson())
.addField(apiDefs::key::isConnectEvent, isConnectEvent ? QJsonValue(true) : QJsonValue())
.build();
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
appendProtocolDataToApiPayload(serviceProtocol, protocolData, apiPayload);
if (isConnectEvent) {
apiPayload[apiDefs::key::isConnectEvent] = true;
}
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;
@@ -428,41 +435,31 @@ 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 errorCode = extractServerConfigJsonFromResponse(responseBody, serviceProtocol, protocolData, serverConfigJson);
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;
newApiV2->nameOverriddenByUser = true;
}
@@ -471,50 +468,6 @@ ErrorCode SubscriptionController::applyUpdatedServiceConfig(const QString &serve
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 apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
.addField(apiDefs::key::userCountryCode, apiV2->apiConfig.userCountryCode)
.addField(apiDefs::key::serverCountryCode, newCountryCode)
.addField(apiDefs::key::serviceType, apiV2->serviceType())
.addField(apiDefs::key::serviceProtocol, serviceProtocol)
.addField(apiDefs::key::publicKey, publicKeyForProtocol(serviceProtocol, protocolData))
.addField(apiDefs::key::authData, apiV2->authData.toJson())
.addField(apiDefs::key::captchaId, captchaId)
.addField(apiDefs::key::captchaSolution, normalizeCaptchaSolution(captchaSolution))
.addField(apiDefs::key::isConnectEvent, isConnectEvent ? QJsonValue(true) : QJsonValue())
.build();
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);
@@ -526,12 +479,18 @@ ErrorCode SubscriptionController::deactivateDevice(const QString &serverId)
return ErrorCode::NoError;
}
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
.addField(apiDefs::key::userCountryCode, apiV2->apiConfig.userCountryCode)
.addField(apiDefs::key::serverCountryCode, apiV2->apiConfig.serverCountryCode)
.addField(apiDefs::key::serviceType, apiV2->serviceType())
.addField(apiDefs::key::authData, apiV2->authData.toJson())
.build();
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,
apiV2->apiConfig.serverCountryCode,
apiV2->serviceType(),
"",
authDataJson };
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
const bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
QByteArray responseBody;
@@ -557,13 +516,18 @@ ErrorCode SubscriptionController::deactivateExternalDevice(const QString &server
return ErrorCode::NoError;
}
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
.addField(apiDefs::key::installationUuid, uuid)
.addField(apiDefs::key::userCountryCode, apiV2->apiConfig.userCountryCode)
.addField(apiDefs::key::serverCountryCode, serverCountryCode)
.addField(apiDefs::key::serviceType, apiV2->serviceType())
.addField(apiDefs::key::authData, apiV2->authData.toJson())
.build();
QJsonObject authDataJson = apiV2->authData.toJson();
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_appSettingsRepository->getAppLanguage().name().split("_").first(),
uuid,
apiV2->apiConfig.userCountryCode,
serverCountryCode,
apiV2->serviceType(),
"",
authDataJson };
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
const bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
QByteArray responseBody;
@@ -591,14 +555,19 @@ ErrorCode SubscriptionController::exportNativeConfig(const QString &serverId, co
QString protocol = configKey::awg;
ProtocolData protocolData = generateProtocolData(protocol);
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
.addField(apiDefs::key::userCountryCode, apiV2->apiConfig.userCountryCode)
.addField(apiDefs::key::serverCountryCode, serverCountryCode)
.addField(apiDefs::key::serviceType, apiV2->serviceType())
.addField(apiDefs::key::serviceProtocol, protocol)
.addField(apiDefs::key::publicKey, publicKeyForProtocol(protocol, protocolData))
.addField(apiDefs::key::authData, apiV2->authData.toJson())
.build();
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,
serverCountryCode,
apiV2->serviceType(),
protocol,
authDataJson };
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
appendProtocolDataToApiPayload(protocol, protocolData, apiPayload);
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/native_config"), apiPayload, responseBody, isTestPurchase);
@@ -621,13 +590,18 @@ ErrorCode SubscriptionController::revokeNativeConfig(const QString &serverId, co
const bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
QString protocol = configKey::awg;
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
.addField(apiDefs::key::userCountryCode, apiV2->apiConfig.userCountryCode)
.addField(apiDefs::key::serverCountryCode, serverCountryCode)
.addField(apiDefs::key::serviceType, apiV2->serviceType())
.addField(apiDefs::key::serviceProtocol, protocol)
.addField(apiDefs::key::authData, apiV2->authData.toJson())
.build();
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,
serverCountryCode,
apiV2->serviceType(),
protocol,
authDataJson };
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/revoke_native_config"), apiPayload, responseBody, isTestPurchase);
@@ -658,20 +632,19 @@ ErrorCode SubscriptionController::prepareVpnKeyExport(const QString &serverId, Q
return ErrorCode::NoError;
}
ErrorCode SubscriptionController::validateAndUpdateConfig(const QString &serverId, bool hasInstalledContainers,
CaptchaInfo *captchaInfoOut, ProtocolData *usedProtocolDataOut)
ErrorCode SubscriptionController::validateAndUpdateConfig(const QString &serverId, bool hasInstalledContainers)
{
if (!m_serversRepository->apiV2Config(serverId).has_value()) {
return ErrorCode::NoError;
}
if (!hasInstalledContainers) {
return updateServiceFromGateway(serverId, "", true, captchaInfoOut, usedProtocolDataOut);
return updateServiceFromGateway(serverId, "", true);
}
if (isApiKeyExpired(serverId)) {
qDebug() << "attempt to update api config by expires_at event";
return updateServiceFromGateway(serverId, "", true, captchaInfoOut, usedProtocolDataOut);
return updateServiceFromGateway(serverId, "", true);
}
return ErrorCode::NoError;
@@ -692,7 +665,7 @@ void SubscriptionController::removeApiConfig(const QString &serverId)
.arg(hostName)
.arg("");
SWIFT_BRIDGE_NAMESPACE::removeVPNC(vpncName.toStdString());
AmneziaVPN::removeVPNC(vpncName.toStdString());
#endif
apiV2->dns1.clear();
@@ -763,36 +736,6 @@ 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)
@@ -934,13 +877,20 @@ ErrorCode SubscriptionController::getAccountInfo(const QString &serverId, QJsonO
}
bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
.addField(apiDefs::key::userCountryCode, apiV2->apiConfig.userCountryCode)
.addField(apiDefs::key::serviceType, apiV2->serviceType())
.addField(apiDefs::key::authData, apiV2->authData.toJson())
.addField(apiDefs::key::cliVersion, QString(APP_VERSION))
.addField(apiDefs::key::subscriptionStatus, getSubscriptionStatusForRenewal(apiV2->apiConfig))
.build();
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,
"",
apiV2->serviceType(),
"",
authDataJson };
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
apiPayload[apiDefs::key::cliVersion] = QString(APP_VERSION);
apiPayload[apiDefs::key::subscriptionStatus] = getSubscriptionStatusForRenewal(apiV2->apiConfig);
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/account_info"), apiPayload, responseBody, isTestPurchase);
@@ -948,8 +898,7 @@ ErrorCode SubscriptionController::getAccountInfo(const QString &serverId, QJsonO
return errorCode;
}
accountInfo = QJsonDocument::fromJson(responseBody).object();
accountInfo = QJsonDocument::fromJson(responseBody).object();
return ErrorCode::NoError;
}
@@ -966,19 +915,25 @@ QFuture<QPair<ErrorCode, QString>> SubscriptionController::getRenewalLink(const
}
bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
.addField(apiDefs::key::userCountryCode, apiV2->apiConfig.userCountryCode)
.addField(apiDefs::key::serviceType, apiV2->serviceType())
.addField(apiDefs::key::authData, apiV2->authData.toJson())
.addField(apiDefs::key::cliVersion, QString(APP_VERSION))
.addField(apiDefs::key::subscriptionStatus, getSubscriptionStatusForRenewal(apiV2->apiConfig))
.build();
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,
"",
apiV2->serviceType(),
"",
authDataJson };
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
apiPayload[apiDefs::key::cliVersion] = QString(APP_VERSION);
apiPayload[apiDefs::key::subscriptionStatus] = getSubscriptionStatusForRenewal(apiV2->apiConfig);
auto gatewayController = QSharedPointer<GatewayController>::create(m_appSettingsRepository->getGatewayEndpoint(isTestPurchase),
m_appSettingsRepository->isDevGatewayEnv(isTestPurchase),
apiDefs::requestTimeoutMsecs,
m_appSettingsRepository->isStrictKillSwitchEnabled(),
m_appSettingsRepository);
m_appSettingsRepository->isStrictKillSwitchEnabled());
auto postFuture = gatewayController->postAsync(QString("%1v1/renewal_link"), apiPayload);
auto *watcher = new QFutureWatcher<QPair<ErrorCode, QByteArray>>();
QObject::connect(watcher, &QFutureWatcher<QPair<ErrorCode, QByteArray>>::finished,
@@ -1000,48 +955,3 @@ QFuture<QPair<ErrorCode, QString>> SubscriptionController::getRenewalLink(const
return promise->future();
}
ErrorCode SubscriptionController::resolveImportServiceCaptcha(const QString &userCountryCode,
const QString &serviceType,
const QString &serviceProtocol,
const ProtocolData &protocolData,
const QString &captchaId,
const QString &captchaSolution,
CaptchaInfo *retryCaptchaOut)
{
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
.addField(apiDefs::key::userCountryCode, userCountryCode)
.addField(apiDefs::key::serviceType, serviceType)
.addField(apiDefs::key::serviceProtocol, serviceProtocol)
.addField(apiDefs::key::publicKey, publicKeyForProtocol(serviceProtocol, protocolData))
.addField(apiDefs::key::captchaId, captchaId)
.addField(apiDefs::key::captchaSolution, normalizeCaptchaSolution(captchaSolution))
.build();
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody);
if (errorCode != ErrorCode::NoError) {
if (retryCaptchaOut
&& (errorCode == ErrorCode::ApiCaptchaInvalidError || errorCode == ErrorCode::ApiCaptchaRefreshError
|| errorCode == ErrorCode::ApiCaptchaRequiredError)) {
fillCaptchaInfoFromResponse(responseBody, *retryCaptchaOut);
}
return errorCode;
}
QJsonObject serverConfigJson;
errorCode = extractServerConfigJsonFromResponse(responseBody, serviceProtocol, protocolData, serverConfigJson);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
updateApiConfigInJson(serverConfigJson, serviceType, serviceProtocol, userCountryCode, responseBody);
if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
return ErrorCode::InternalError;
}
ApiV2ServerConfig apiV2ServerConfig = ApiV2ServerConfig::fromJson(serverConfigJson);
m_serversRepository->addServer(QString(), apiV2ServerConfig.toJson(),
serverConfigUtils::configTypeFromJson(apiV2ServerConfig.toJson()));
return ErrorCode::NoError;
}

View File

@@ -27,23 +27,29 @@ public:
QString xrayUuid;
};
struct CaptchaInfo {
QString captchaId;
QString captchaImageBase64;
QString hint;
bool isRequired = false;
struct GatewayRequestData
{
QString osVersion;
QString appVersion;
QString appLanguage;
QString installationUuid;
QString userCountryCode;
QString serverCountryCode;
QString serviceType;
QString serviceProtocol;
QJsonObject authData;
QJsonObject toJsonObject() const;
};
explicit SubscriptionController(SecureServersRepository* serversRepository,
SecureAppSettingsRepository* appSettingsRepository);
ProtocolData generateProtocolData(const QString &protocol);
static QString publicKeyForProtocol(const QString &protocol, const ProtocolData &protocolData);
void appendProtocolDataToApiPayload(const QString &protocol, const ProtocolData &protocolData, QJsonObject &apiPayload);
ErrorCode importServiceFromGateway(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const ProtocolData &protocolData,
CaptchaInfo &captchaInfo);
const QString &serviceProtocol, const ProtocolData &protocolData);
ErrorCode importTrialFromGateway(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const QString &email);
@@ -52,12 +58,7 @@ public:
const QString &transactionId, bool isTestPurchase,
int *duplicateServerIndex = nullptr);
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 updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent);
ErrorCode deactivateDevice(const QString &serverId);
@@ -69,8 +70,7 @@ public:
ErrorCode prepareVpnKeyExport(const QString &serverId, QString &vpnKey);
ErrorCode validateAndUpdateConfig(const QString &serverId, bool hasInstalledContainers,
CaptchaInfo *captchaInfoOut = nullptr, ProtocolData *usedProtocolDataOut = nullptr);
ErrorCode validateAndUpdateConfig(const QString &serverId, bool hasInstalledContainers);
void removeApiConfig(const QString &serverId);
@@ -78,8 +78,6 @@ 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);
@@ -100,19 +98,12 @@ public:
AppStoreRestoreResult processAppStoreRestore(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol);
ErrorCode resolveImportServiceCaptcha(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const ProtocolData &protocolData,
const QString &captchaId, const QString &captchaSolution,
CaptchaInfo *retryCaptchaOut = nullptr);
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,8 +6,9 @@
#include "core/utils/protocolEnum.h"
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/payloadSender.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/utilities.h"
#include "core/utils/networkUtilities.h"
#include "core/utils/serverConfigUtils.h"
#include "version.h"
#include "core/utils/containerEnum.h"
@@ -31,6 +32,7 @@ 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,99 +46,19 @@ bool ConnectionController::isConnected() const
void ConnectionController::setConnectionState(Vpn::ConnectionState state)
{
emit connectionStateChanged(state);
}
ErrorCode ConnectionController::defaultContainerForServer(const QString &serverId, DockerContainer &container) const
{
const auto kind = m_serversRepository->serverKind(serverId);
switch (kind) {
case serverConfigUtils::ConfigType::SelfHostedAdmin: {
const auto cfg = m_serversRepository->selfHostedAdminConfig(serverId);
if (!cfg.has_value()) {
return ErrorCode::InternalError;
}
container = cfg->defaultContainer;
return ErrorCode::NoError;
if (m_vpnConnection) {
emit setConnectionStateRequested(state);
}
case serverConfigUtils::ConfigType::SelfHostedUser: {
const auto cfg = m_serversRepository->selfHostedUserConfig(serverId);
if (!cfg.has_value()) {
return ErrorCode::InternalError;
}
container = cfg->defaultContainer;
return ErrorCode::NoError;
}
case serverConfigUtils::ConfigType::Native: {
const auto cfg = m_serversRepository->nativeConfig(serverId);
if (!cfg.has_value()) {
return ErrorCode::InternalError;
}
container = cfg->defaultContainer;
return ErrorCode::NoError;
}
case serverConfigUtils::ConfigType::AmneziaPremiumV2:
case serverConfigUtils::ConfigType::AmneziaFreeV3:
case serverConfigUtils::ConfigType::ExternalPremium: {
const auto cfg = m_serversRepository->apiV2Config(serverId);
if (!cfg.has_value()) {
return ErrorCode::InternalError;
}
container = cfg->defaultContainer;
return ErrorCode::NoError;
}
case serverConfigUtils::ConfigType::AmneziaPremiumV1:
case serverConfigUtils::ConfigType::AmneziaFreeV2:
return ErrorCode::LegacyApiV1NotSupportedError;
case serverConfigUtils::ConfigType::Invalid:
default:
return ErrorCode::InternalError;
}
}
ErrorCode ConnectionController::isConnectionSupported(const QString &serverId) const
{
if (serverId.isEmpty()) {
return ErrorCode::InternalError;
}
if (!isServiceReady()) {
return ErrorCode::AmneziaServiceNotRunning;
}
const serverConfigUtils::ConfigType kind = m_serversRepository->serverKind(serverId);
if (serverConfigUtils::isLegacyApiSubscription(kind)) {
return ErrorCode::LegacyApiV1NotSupportedError;
}
DockerContainer container = DockerContainer::None;
const ErrorCode errorCode = defaultContainerForServer(serverId, container);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
if (container == DockerContainer::None) {
if (serverConfigUtils::isApiV2Subscription(kind)) {
return ErrorCode::NoError;
}
return ErrorCode::NoInstalledContainersError;
}
if (ContainerUtils::isUnsupportedContainer(container)) {
return ErrorCode::LegacyContainerNotSupportedError;
}
if (!isContainerSupported(container)) {
return ErrorCode::NotSupportedOnThisPlatform;
}
return ErrorCode::NoError;
}
ErrorCode ConnectionController::prepareConnection(const QString &serverId,
QJsonObject& vpnConfiguration,
DockerContainer& container)
{
if (!isServiceReady()) {
return ErrorCode::AmneziaServiceNotRunning;
}
ContainerConfig containerConfigModel;
QPair<QString, QString> dns;
QString hostName;
@@ -145,15 +67,13 @@ ErrorCode ConnectionController::prepareConnection(const QString &serverId,
bool isApiConfig = false;
const auto kind = m_serversRepository->serverKind(serverId);
const QString primaryDns = m_appSettingsRepository->primaryDns();
const QString secondaryDns = m_appSettingsRepository->secondaryDns();
switch (kind) {
case serverConfigUtils::ConfigType::SelfHostedAdmin: {
const auto cfg = m_serversRepository->selfHostedAdminConfig(serverId);
if (!cfg.has_value()) return ErrorCode::InternalError;
container = cfg->defaultContainer;
containerConfigModel = cfg->containerConfig(container);
dns = cfg->getDnsPair(m_appSettingsRepository->useAmneziaDns(), primaryDns, secondaryDns);
dns = { cfg->dns1, cfg->dns2 };
hostName = cfg->hostName;
description = cfg->description;
break;
@@ -163,7 +83,7 @@ ErrorCode ConnectionController::prepareConnection(const QString &serverId,
if (!cfg.has_value()) return ErrorCode::InternalError;
container = cfg->defaultContainer;
containerConfigModel = cfg->containerConfig(container);
dns = cfg->getDnsPair(primaryDns, secondaryDns);
dns = { cfg->dns1, cfg->dns2 };
hostName = cfg->hostName;
description = cfg->description;
break;
@@ -173,7 +93,7 @@ ErrorCode ConnectionController::prepareConnection(const QString &serverId,
if (!cfg.has_value()) return ErrorCode::InternalError;
container = cfg->defaultContainer;
containerConfigModel = cfg->containerConfig(container);
dns = cfg->getDnsPair(primaryDns, secondaryDns);
dns = { cfg->dns1, cfg->dns2 };
hostName = cfg->hostName;
description = cfg->description;
break;
@@ -185,7 +105,7 @@ ErrorCode ConnectionController::prepareConnection(const QString &serverId,
if (!cfg.has_value()) return ErrorCode::InternalError;
container = cfg->defaultContainer;
containerConfigModel = cfg->containerConfig(container);
dns = cfg->getDnsPair(primaryDns, secondaryDns);
dns = { cfg->dns1, cfg->dns2 };
hostName = cfg->hostName;
description = cfg->description;
configVersion = serverConfigUtils::ConfigSource::AmneziaGateway;
@@ -200,6 +120,20 @@ ErrorCode ConnectionController::prepareConnection(const QString &serverId,
return ErrorCode::InternalError;
}
if (!isContainerSupported(container)) {
return ErrorCode::NotSupportedOnThisPlatform;
}
if (dns.first.isEmpty() || !NetworkUtilities::checkIPv4Format(dns.first)) {
if (m_appSettingsRepository->useAmneziaDns()) {
dns.first = protocols::dns::amneziaDnsIp;
} else {
dns.first = m_appSettingsRepository->primaryDns();
}
}
if (dns.second.isEmpty() || !NetworkUtilities::checkIPv4Format(dns.second)) {
dns.second = m_appSettingsRepository->secondaryDns();
}
vpnConfiguration = createConnectionConfiguration(dns, isApiConfig, hostName, description, configVersion,
containerConfigModel, container);
@@ -216,11 +150,6 @@ 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

@@ -34,8 +34,6 @@ public:
QJsonObject& vpnConfiguration,
DockerContainer& container);
ErrorCode isConnectionSupported(const QString &serverId) const;
ErrorCode openConnection(const QString &serverId);
void closeConnection();
@@ -67,6 +65,7 @@ signals:
void connectionStateChanged(Vpn::ConnectionState state);
void openConnectionRequested(const QString &serverId, DockerContainer container, const QJsonObject &vpnConfiguration);
void closeConnectionRequested();
void setConnectionStateRequested(Vpn::ConnectionState state);
void killSwitchModeChangedRequested(bool enabled);
#ifdef Q_OS_ANDROID
@@ -74,8 +73,6 @@ signals:
#endif
private:
ErrorCode defaultContainerForServer(const QString &serverId, DockerContainer &container) const;
SecureServersRepository* m_serversRepository;
SecureAppSettingsRepository* m_appSettingsRepository;
VpnConnection* m_vpnConnection;

View File

@@ -10,7 +10,6 @@
#include "core/controllers/coreSignalHandlers.h"
#include "logger.h"
#include "secureQSettings.h"
#include "core/utils/appUiConfig.h"
#if defined(Q_OS_ANDROID)
#include "core/utils/installedAppsImageProvider.h"
@@ -19,12 +18,11 @@
#if defined(Q_OS_IOS)
#include "platforms/ios/ios_controller.h"
#include "core/utils/swiftBridge.h"
#include <AmneziaVPN-Swift.h>
#endif
CoreController::CoreController(const QSharedPointer<VpnConnection> &vpnConnection, SecureQSettings* settings,
QQmlApplicationEngine *engine, QObject *parent,
bool skipPlatformControllerInit)
QQmlApplicationEngine *engine, QObject *parent)
: QObject(parent), m_vpnConnection(vpnConnection), m_settings(settings), m_engine(engine)
{
initRepositories();
@@ -33,10 +31,8 @@ CoreController::CoreController(const QSharedPointer<VpnConnection> &vpnConnectio
initControllers();
initSignalHandlers();
if (!skipPlatformControllerInit) {
initAndroidController();
initAppleController();
}
initAndroidController();
initAppleController();
initLogging();
m_translator = new QTranslator(this);
@@ -90,9 +86,6 @@ void CoreController::initModels()
m_xrayConfigModel = new XrayConfigModel(this);
setQmlContextProperty("XrayConfigModel", m_xrayConfigModel);
m_xrayConfigSnapshotsModel = new XrayConfigSnapshotsModel(m_appSettingsRepository, m_xrayConfigModel, this);
setQmlContextProperty("XrayConfigSnapshotsModel", m_xrayConfigSnapshotsModel);
m_torConfigModel = new TorConfigModel(this);
setQmlContextProperty("TorConfigModel", m_torConfigModel);
@@ -107,12 +100,6 @@ void CoreController::initModels()
m_socks5ConfigModel = new Socks5ProxyConfigModel(this);
setQmlContextProperty("Socks5ProxyConfigModel", m_socks5ConfigModel);
m_mtProxyConfigModel = new MtProxyConfigModel(this);
setQmlContextProperty("MtProxyConfigModel", m_mtProxyConfigModel);
m_telemtConfigModel = new TelemtConfigModel(this);
setQmlContextProperty("TelemtConfigModel", m_telemtConfigModel);
m_clientManagementModel = new ClientManagementModel(this);
setQmlContextProperty("ClientManagementModel", m_clientManagementModel);
@@ -182,8 +169,7 @@ void CoreController::initControllers()
#ifdef Q_OS_WINDOWS
m_ikev2ConfigModel,
#endif
m_sftpConfigModel, m_socks5ConfigModel, m_mtProxyConfigModel, m_telemtConfigModel,
m_connectionController, this);
m_sftpConfigModel, m_socks5ConfigModel, this);
setQmlContextProperty("InstallController", m_installUiController);
m_importController = new ImportUiController(m_importCoreController, this);
@@ -195,7 +181,7 @@ void CoreController::initControllers()
m_languageUiController = new LanguageUiController(m_settingsController, m_languageModel, this);
setQmlContextProperty("LanguageUiController", m_languageUiController);
m_settingsUiController = new SettingsUiController(m_settingsController, m_serversController, this);
m_settingsUiController = new SettingsUiController(m_settingsController, m_serversController, m_languageUiController, this);
setQmlContextProperty("SettingsController", m_settingsUiController);
m_pageController = new PageController(m_serversController, m_settingsController, this);
@@ -216,17 +202,12 @@ void CoreController::initControllers()
m_systemController = new SystemController(this);
setQmlContextProperty("SystemController", m_systemController);
m_networkReachabilityController = new NetworkReachabilityController(this);
setQmlContextProperty("NetworkReachabilityController", m_networkReachabilityController);
setQmlContextProperty("NetworkReachability", m_networkReachabilityController);
m_servicesCatalogUiController = new ServicesCatalogUiController(m_servicesCatalogController, m_apiServicesModel, this);
setQmlContextProperty("ServicesCatalogUiController", m_servicesCatalogUiController);
m_subscriptionUiController = new SubscriptionUiController(m_serversController, m_apiServicesModel, m_servicesCatalogController, m_subscriptionController,
m_apiSubscriptionPlansModel, m_apiBenefitsModel, m_apiAccountInfoModel,
m_apiCountryModel, m_apiDevicesModel, m_settingsController,
m_connectionController, this);
m_apiCountryModel, m_apiDevicesModel, m_settingsController, this);
setQmlContextProperty("SubscriptionUiController", m_subscriptionUiController);
m_apiNewsUiController = new ApiNewsUiController(m_newsModel, m_newsController, this);
@@ -259,7 +240,7 @@ void CoreController::initAppleController()
{
#ifdef Q_OS_IOS
IosController::Instance()->initialize();
QTimer::singleShot(0, this, [this]() { SWIFT_BRIDGE_NAMESPACE::toggleScreenshots(m_appSettingsRepository->isScreenshotsEnabled()); });
QTimer::singleShot(0, this, [this]() { AmneziaVPN::toggleScreenshots(m_appSettingsRepository->isScreenshotsEnabled()); });
#endif
}
@@ -286,10 +267,6 @@ 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)
@@ -299,15 +276,15 @@ void CoreController::updateTranslator(const QLocale &locale)
}
QStringList availableTranslations;
QDirIterator it(":/translations", QStringList(APP_TS_PREFIX "_*.qm"), QDir::Files);
QDirIterator it(":/translations", QStringList("amneziavpn_*.qm"), QDir::Files);
while (it.hasNext()) {
availableTranslations << it.next();
}
// This code allow to load translation for the language only, without country code
const QString lang = locale.name().split("_").first();
const QString translationFilePrefix = QString(":/translations/" APP_TS_PREFIX "_") + lang;
QString strFileName = QString(":/translations/" APP_TS_PREFIX "_%1.qm").arg(locale.name());
const QString translationFilePrefix = QString(":/translations/amneziavpn_") + lang;
QString strFileName = QString(":/translations/amneziavpn_%1.qm").arg(locale.name());
for (const QString &translation : availableTranslations) {
if (translation.contains(translationFilePrefix)) {
strFileName = translation;
@@ -318,7 +295,7 @@ void CoreController::updateTranslator(const QLocale &locale)
if (m_translator->load(strFileName)) {
QCoreApplication::installTranslator(m_translator);
} else {
if (m_translator->load(QString(":/translations/" APP_TS_PREFIX "_en.qm"))) {
if (m_translator->load(QString(":/translations/amneziavpn_en.qm"))) {
QCoreApplication::installTranslator(m_translator);
}
}
@@ -352,6 +329,9 @@ void CoreController::openConnectionByIndex(int serverIndex)
if (serverId.isEmpty()) {
return;
}
if (m_serversModel) {
m_serversModel->setProcessedServerIndex(serverIndex);
}
if (m_serversController) {
m_serversController->setDefaultServer(serverId);
}

View File

@@ -28,7 +28,6 @@
#include "ui/controllers/languageUiController.h"
#include "ui/controllers/updateUiController.h"
#include "ui/controllers/api/servicesCatalogUiController.h"
#include "ui/controllers/networkReachabilityController.h"
#include "core/controllers/serversController.h"
#include "core/controllers/selfhosted/usersController.h"
@@ -65,15 +64,11 @@
#include "ui/models/protocols/openvpnConfigModel.h"
#include "ui/models/protocols/wireguardConfigModel.h"
#include "ui/models/protocols/xrayConfigModel.h"
#include "ui/models/protocols/xrayConfigSnapshotsModel.h"
#include "ui/models/protocolsModel.h"
#include "ui/models/services/torConfigModel.h"
#include "ui/models/serversModel.h"
#include "ui/models/services/sftpConfigModel.h"
#include "ui/models/services/socks5ProxyConfigModel.h"
#include "ui/models/services/mtProxyConfigModel.h"
#include "ui/models/services/telemtConfigModel.h"
#include "ui/models/ipSplitTunnelingModel.h"
#include "ui/models/newsModel.h"
@@ -82,16 +77,37 @@
#endif
class CoreSignalHandlers;
class TestMultipleImports;
class TestAdminSelfHostedExport;
class TestServerEdit;
class TestDefaultServerChange;
class TestServerEdgeCases;
class TestSignalOrder;
class TestServersModelSync;
class TestComplexOperations;
class TestSettingsSignals;
class TestUiServersModelAndController;
class TestSelfHostedServerSetup;
class CoreController : public QObject
{
Q_OBJECT
friend class CoreSignalHandlers;
friend class TestMultipleImports;
friend class TestAdminSelfHostedExport;
friend class TestServerEdit;
friend class TestDefaultServerChange;
friend class TestServerEdgeCases;
friend class TestSignalOrder;
friend class TestServersModelSync;
friend class TestComplexOperations;
friend class TestSettingsSignals;
friend class TestUiServersModelAndController;
friend class TestSelfHostedServerSetup;
public:
explicit CoreController(const QSharedPointer<VpnConnection> &vpnConnection, SecureQSettings* settings,
QQmlApplicationEngine *engine, QObject *parent = nullptr,
bool skipPlatformControllerInit = false);
QQmlApplicationEngine *engine, QObject *parent = nullptr);
PageController* pageController() const;
void setQmlRoot();
@@ -104,36 +120,6 @@ signals:
void translationsUpdated();
void websiteUrlChanged(const QString &newUrl);
protected:
SecureServersRepository* serversRepositoryProtected() const { return m_serversRepository; }
SecureAppSettingsRepository* appSettingsRepositoryProtected() const { return m_appSettingsRepository; }
ServersModel* serversModelProtected() const { return m_serversModel; }
ContainersModel* containersModelProtected() const { return m_containersModel; }
ApiServicesModel* apiServicesModelProtected() const { return m_apiServicesModel; }
NewsModel* newsModelProtected() const { return m_newsModel; }
AllowedDnsModel* allowedDnsModelProtected() const { return m_allowedDnsModel; }
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; }
InstallController* installControllerProtected() const { return m_installController; }
ServersController* serversControllerProtected() const { return m_serversController; }
SettingsUiController* settingsUiControllerProtected() const { return m_settingsUiController; }
SettingsController* settingsControllerProtected() const { return m_settingsController; }
AllowedDnsUiController* allowedDnsUiControllerProtected() const { return m_allowedDnsUiController; }
AllowedDnsController* allowedDnsControllerProtected() const { return m_allowedDnsController; }
LanguageUiController* languageUiControllerProtected() const { return m_languageUiController; }
IpSplitTunnelingController* ipSplitTunnelingControllerProtected() const { return m_ipSplitTunnelingController; }
IpSplitTunnelingUiController* ipSplitTunnelingUiControllerProtected() const { return m_ipSplitTunnelingUiController; }
AppSplitTunnelingController* appSplitTunnelingControllerProtected() const { return m_appSplitTunnelingController; }
AppSplitTunnelingUiController* appSplitTunnelingUiControllerProtected() const { return m_appSplitTunnelingUiController; }
ServersUiController* serversUiControllerProtected() const { return m_serversUiController; }
ServicesCatalogUiController* servicesCatalogUiControllerProtected() const { return m_servicesCatalogUiController; }
ApiNewsUiController* apiNewsUiControllerProtected() const { return m_apiNewsUiController; }
private:
void initRepositories();
void initCoreControllers();
@@ -170,7 +156,6 @@ private:
ServersUiController* m_serversUiController;
IpSplitTunnelingUiController* m_ipSplitTunnelingUiController;
SystemController* m_systemController;
NetworkReachabilityController* m_networkReachabilityController;
AppSplitTunnelingUiController* m_appSplitTunnelingUiController;
AllowedDnsUiController* m_allowedDnsUiController;
LanguageUiController* m_languageUiController;
@@ -215,7 +200,6 @@ private:
OpenVpnConfigModel* m_openVpnConfigModel;
XrayConfigModel* m_xrayConfigModel;
XrayConfigSnapshotsModel* m_xrayConfigSnapshotsModel;
TorConfigModel* m_torConfigModel;
WireGuardConfigModel* m_wireGuardConfigModel;
AwgConfigModel* m_awgConfigModel;
@@ -224,8 +208,6 @@ private:
#endif
SftpConfigModel* m_sftpConfigModel;
Socks5ProxyConfigModel* m_socks5ConfigModel;
MtProxyConfigModel* m_mtProxyConfigModel;
TelemtConfigModel* m_telemtConfigModel;
CoreSignalHandlers* m_signalHandlers;
};

View File

@@ -1,7 +1,6 @@
#include "coreSignalHandlers.h"
#include <QTimer>
#include <QtConcurrent>
#include "core/utils/selfhosted/sshSession.h"
#include "core/utils/errorCodes.h"
@@ -34,6 +33,7 @@
#include "core/controllers/connectionController.h"
#include "ui/models/clientManagementModel.h"
#include "ui/controllers/api/apiNewsUiController.h"
#include "ui/models/api/apiCountryModel.h"
#include "ui/models/containersModel.h"
#include "core/utils/containerEnum.h"
@@ -48,7 +48,7 @@
#ifdef Q_OS_IOS
#include "platforms/ios/ios_controller.h"
#include "core/utils/swiftBridge.h"
#include <AmneziaVPN-Swift.h>
#endif
CoreSignalHandlers::CoreSignalHandlers(CoreController* coreController, QObject* parent)
@@ -125,9 +125,9 @@ void CoreSignalHandlers::initInstallControllerHandler()
{
connect(m_coreController->m_installController, &InstallController::serverIsBusy, m_coreController->m_installUiController, &InstallUiController::serverIsBusy);
connect(m_coreController->m_installUiController, &InstallUiController::cancelInstallation, m_coreController->m_installController, &InstallController::cancelInstallation);
connect(m_coreController->m_serversUiController, &ServersUiController::processedServerIdChanged,
m_coreController->m_installUiController, [this](const QString &serverId) {
if (!serverId.isEmpty()) {
connect(m_coreController->m_serversUiController, &ServersUiController::processedServerIndexChanged,
m_coreController->m_installUiController, [this](int serverIndex) {
if (serverIndex >= 0) {
m_coreController->m_installUiController->clearProcessedServerCredentials();
}
});
@@ -145,9 +145,7 @@ void CoreSignalHandlers::initExportControllerHandler()
});
connect(m_coreController->m_exportController, &ExportController::revokeClientRequested, this,
[this](const QString &serverId, int row, DockerContainer container) {
QtConcurrent::run([this, serverId, row, container]() {
m_coreController->m_usersController->revokeClient(serverId, row, container);
});
m_coreController->m_usersController->revokeClient(serverId, row, container);
});
connect(m_coreController->m_exportController, &ExportController::renameClientRequested, this,
[this](const QString &serverId, int row, const QString &clientName, DockerContainer container) {
@@ -158,17 +156,15 @@ void CoreSignalHandlers::initExportControllerHandler()
void CoreSignalHandlers::initImportControllerHandler()
{
connect(m_coreController->m_importCoreController, &ImportController::importFinished, this, [this]() {
if (m_coreController->m_connectionUiController->isConnected()) {
return;
}
const int newServerIndex = m_coreController->m_serversController->getServersCount() - 1;
const QString serverId = m_coreController->m_serversController->getServerId(newServerIndex);
if (!serverId.isEmpty()) {
m_coreController->m_serversController->setDefaultServer(serverId);
}
if (m_coreController->m_serversUiController) {
m_coreController->m_serversUiController->setProcessedServerId(serverId);
if (!m_coreController->m_connectionController->isConnected()) {
int newServerIndex = m_coreController->m_serversController->getServersCount() - 1;
const QString serverId = m_coreController->m_serversController->getServerId(newServerIndex);
if (!serverId.isEmpty()) {
m_coreController->m_serversController->setDefaultServer(serverId);
}
if (m_coreController->m_serversUiController) {
m_coreController->m_serversUiController->setProcessedServerId(serverId);
}
}
});
}
@@ -180,14 +176,17 @@ void CoreSignalHandlers::initApiCountryModelUpdateHandler()
if (processedServerId.isEmpty()) {
return;
}
QJsonArray availableCountries;
QString serverCountryCode;
const auto apiV2 = m_coreController->m_serversRepository->apiV2Config(processedServerId);
if (!apiV2.has_value()) {
return;
if (apiV2.has_value()) {
availableCountries = apiV2->apiConfig.availableCountries;
serverCountryCode = apiV2->apiConfig.serverCountryCode;
}
m_coreController->m_apiCountryModel->updateModel(apiV2->apiConfig.availableCountries,
apiV2->apiConfig.serverCountryCode);
m_coreController->m_apiCountryModel->updateModel(availableCountries, serverCountryCode);
});
}
@@ -206,12 +205,12 @@ void CoreSignalHandlers::initAdminConfigRevokedHandler()
connect(m_coreController->m_installController, &InstallController::clientRevocationRequested, this,
[this](const QString &serverId, const ContainerConfig &containerConfig, DockerContainer container) {
m_coreController->m_usersController->revokeClient(serverId, containerConfig, container);
}, Qt::DirectConnection);
});
connect(m_coreController->m_installController, &InstallController::clientAppendRequested, this,
[this](const QString &serverId, const QString &clientId, const QString &clientName, DockerContainer container) {
m_coreController->m_usersController->appendClient(serverId, clientId, clientName, container);
}, Qt::DirectConnection);
});
connect(m_coreController->m_usersController, &UsersController::adminConfigRevoked, m_coreController->m_installController,
&InstallController::clearCachedProfile);
@@ -238,16 +237,13 @@ void CoreSignalHandlers::initLanguageHandler()
connect(m_coreController->m_settingsUiController, &SettingsUiController::resetLanguageToSystem, m_coreController->m_languageUiController, [this]() {
m_coreController->m_languageUiController->changeLanguage(m_coreController->m_languageUiController->getSystemLanguageEnum());
});
connect(m_coreController->m_settingsUiController, &SettingsUiController::appLanguageChanged, m_coreController->m_languageUiController, [this]() {
m_coreController->m_languageUiController->onAppLanguageChanged(m_coreController->m_settingsController->getAppLanguage());
});
}
void CoreSignalHandlers::initAutoConnectHandler()
{
if (m_coreController->m_settingsUiController->isAutoConnectEnabled()
&& !m_coreController->m_serversController->getDefaultServerId().isEmpty()) {
QTimer::singleShot(1000, this, [this]() { m_coreController->m_connectionUiController->toggleConnection(); });
QTimer::singleShot(1000, this, [this]() { m_coreController->m_connectionUiController->openConnection(); });
}
}
@@ -288,8 +284,6 @@ void CoreSignalHandlers::initClientManagementModelUpdateHandler()
m_coreController->m_clientManagementModel, &ClientManagementModel::updateModel);
connect(m_coreController->m_usersController, &UsersController::clientRenamed,
m_coreController->m_clientManagementModel, &ClientManagementModel::updateClientName);
connect(m_coreController->m_usersController, &UsersController::revokeFinished,
m_coreController->m_exportController, &ExportController::revokeFinished);
}
void CoreSignalHandlers::initSitesModelUpdateHandler()
@@ -354,9 +348,6 @@ void CoreSignalHandlers::initUnsupportedConnectDrawerHandler()
{
connect(m_coreController->m_subscriptionUiController, &SubscriptionUiController::unsupportedConnectDrawerRequested,
m_coreController->m_pageController, &PageController::unsupportedConnectDrawerRequested);
connect(m_coreController->m_connectionUiController, &ConnectionUiController::unsupportedConnectDrawerRequested,
m_coreController->m_pageController, &PageController::unsupportedConnectDrawerRequested);
}
void CoreSignalHandlers::initStrictKillSwitchHandler()
@@ -413,7 +404,7 @@ void CoreSignalHandlers::initIosImportHandler()
void CoreSignalHandlers::initIosSettingsHandler()
{
#ifdef Q_OS_IOS
connect(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::screenshotsEnabledChanged, [](bool enabled) { SWIFT_BRIDGE_NAMESPACE::toggleScreenshots(enabled); });
connect(m_coreController->m_appSettingsRepository, &SecureAppSettingsRepository::screenshotsEnabledChanged, [](bool enabled) { AmneziaVPN::toggleScreenshots(enabled); });
#endif
}
@@ -440,6 +431,9 @@ 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 <openssl/rsa.h>
#include "QBlockCipher.h"
#include "QRsa.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 "cryptoUtils.h"
#include "core/utils/utilities.h"
#ifdef AMNEZIA_DESKTOP
#include "core/utils/ipcClient.h"
@@ -30,8 +30,6 @@ namespace
constexpr QLatin1String errorResponsePattern1("No active configuration found for");
constexpr QLatin1String errorResponsePattern2("No non-revoked public key found for");
constexpr QLatin1String errorResponsePattern3("Account not found.");
constexpr QLatin1String errorResponsePatternQrSessionNotFound("QR session not found");
constexpr QLatin1String errorResponsePatternSessionNotFound("Session not found");
constexpr QLatin1String updateRequestResponsePattern("client version update is required");
@@ -39,81 +37,20 @@ namespace
constexpr int httpStatusCodeConflict = 409;
constexpr int httpStatusCodeNotImplemented = 501;
constexpr int httpStatusCodePaymentRequired = 402;
constexpr int httpStatusCodeRequestTimeout = 408;
constexpr int httpStatusCodeUnprocessableEntity = 422;
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, SecureAppSettingsRepository *appSettingsRepository,
QObject *parent)
const bool isStrictKillSwitchEnabled, QObject *parent)
: QObject(parent),
m_gatewayEndpoint(gatewayEndpoint),
m_isDevEnvironment(isDevEnvironment),
m_requestTimeoutMsecs(requestTimeoutMsecs),
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled),
m_appSettingsRepository(appSettingsRepository)
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled)
{
}
@@ -147,29 +84,40 @@ GatewayController::EncryptedRequestData GatewayController::prepareRequest(const
}
#endif
encRequestData.key = CryptoUtils::generateRandomBytes(32);
encRequestData.iv = CryptoUtils::generateRandomBytes(32);
encRequestData.salt = CryptoUtils::generateRandomBytes(8);
QSimpleCrypto::QBlockCipher blockCipher;
encRequestData.key = blockCipher.generatePrivateSalt(32);
encRequestData.iv = blockCipher.generatePrivateSalt(32);
encRequestData.salt = blockCipher.generatePrivateSalt(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 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;
}
QByteArray encryptedKeyPayload;
QByteArray encryptedApiPayload;
try {
QSimpleCrypto::QRsa rsa;
QByteArray encryptedKeyPayload = CryptoUtils::rsaEncrypt(QJsonDocument(keyPayload).toJson(), publicKey, RSA_PKCS1_PADDING);
EVP_PKEY_free(publicKey);
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 encryptedApiPayload = CryptoUtils::encryptAes256Cbc(QJsonDocument(apiPayload).toJson(), encRequestData.key, encRequestData.iv);
encryptedKeyPayload = rsa.encrypt(QJsonDocument(keyPayload).toJson(), publicKey, RSA_PKCS1_PADDING);
EVP_PKEY_free(publicKey);
if (encryptedKeyPayload.isEmpty() || encryptedApiPayload.isEmpty()) {
encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(), encRequestData.key, encRequestData.iv,
"", encRequestData.salt);
} catch (...) {
Utils::logException();
qCritical() << "error when encrypting the request body";
encRequestData.errorCode = ErrorCode::ApiConfigDecryptionError;
return encRequestData;
@@ -191,19 +139,15 @@ GatewayController::DecryptionResult GatewayController::tryDecryptResponseBody(co
result.decryptedBody = encryptedResponseBody;
result.isDecryptionSuccessful = false;
QByteArray decrypted = CryptoUtils::decryptAes256Cbc(encryptedResponseBody, key, iv);
if (!decrypted.isEmpty()) {
result.decryptedBody = decrypted;
try {
QSimpleCrypto::QBlockCipher blockCipher;
result.decryptedBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt);
result.isDecryptionSuccessful = true;
} else {
} catch (...) {
result.decryptedBody = encryptedResponseBody;
result.isDecryptionSuccessful = false;
}
qDebug() << "tryDecryptResponseBody: inputBytes=" << encryptedResponseBody.size() << "keyBytes=" << key.size()
<< "ivBytes=" << iv.size() << "saltBytes=" << salt.size() << "replyError=" << replyError
<< "decrypted=" << result.isDecryptionSuccessful << "outputBytes=" << decrypted.size();
return result;
}
@@ -262,9 +206,8 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
bypassProxy(endpoint, serviceType, userCountryCode, requestFunction, replyProcessingFunction);
}
responseBody = decryptionResult.decryptedBody;
const auto errorCode =
apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, responseBody);
auto errorCode =
apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, decryptionResult.decryptedBody);
if (errorCode) {
return errorCode;
}
@@ -274,6 +217,7 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
return ErrorCode::ApiConfigDecryptionError;
}
responseBody = decryptionResult.decryptedBody;
return ErrorCode::NoError;
}
@@ -312,12 +256,13 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode,
decryptionResult.decryptedBody);
if (errorCode) {
promise->addResult(qMakePair(errorCode, decryptionResult.decryptedBody));
promise->addResult(qMakePair(errorCode, QByteArray()));
promise->finish();
return;
}
if (!decryptionResult.isDecryptionSuccessful) {
Utils::logException();
qCritical() << "error when decrypting the request body";
promise->addResult(qMakePair(ErrorCode::ApiConfigDecryptionError, QByteArray()));
promise->finish();
@@ -360,9 +305,8 @@ 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, proxyUrlsCacheKey, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
getProxyUrlsAsync(proxyStorageUrls, 0, [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,
@@ -408,6 +352,8 @@ 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) {
@@ -423,12 +369,10 @@ 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 readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment);
return {};
}
for (const auto &proxyStorageUrl : proxyStorageUrls) {
@@ -443,8 +387,26 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
auto encryptedResponseBody = reply->readAll();
reply->deleteLater();
EVP_PKEY *privateKey = nullptr;
QByteArray responseBody;
if (!decryptProxyUrlsPayload(encryptedResponseBody, m_isDevEnvironment, 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();
qCritical() << "error loading private key from environment variables or decrypting payload" << encryptedResponseBody;
continue;
}
@@ -455,8 +417,6 @@ 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();
@@ -468,7 +428,7 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
reply->deleteLater();
}
}
return readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment);
return {};
}
bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody,
@@ -486,9 +446,7 @@ bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &rep
apiErrorMessage = jsonObj.value(QStringLiteral("message")).toString().trimmed();
}
} else {
qDebug() << "failed to decrypt the data, bypassing the proxy: replyError=" << replyError
<< "bodyBytes=" << responseBody.size()
<< "(an empty body looks the same here as a wrong key, compare with tryDecryptResponseBody above)";
qDebug() << "failed to decrypt the data";
return true;
}
@@ -501,20 +459,15 @@ bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &rep
qDebug() << "the response contains an html tag";
return true;
}
if (apiHttpStatus == httpStatusCodeRequestTimeout) {
qDebug() << "keeping the proxy: apiHttpStatus=" << apiHttpStatus;
return false;
}
if (apiHttpStatus == httpStatusCodeNotFound) {
if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2)
|| responseBody.contains(errorResponsePattern3) || responseBody.contains(errorResponsePatternQrSessionNotFound)
|| responseBody.contains(errorResponsePatternSessionNotFound)) {
|| responseBody.contains(errorResponsePattern3)) {
return false;
} else {
qDebug() << replyError;
return true;
}
}
}
if (apiHttpStatus == httpStatusCodeNotImplemented) {
if (responseBody.contains(updateRequestResponsePattern)) {
return false;
@@ -524,24 +477,18 @@ bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &rep
}
}
if (apiHttpStatus == httpStatusCodeConflict) {
qDebug() << "keeping the proxy: apiHttpStatus=" << apiHttpStatus;
return false;
}
if (apiHttpStatus == httpStatusCodePaymentRequired) {
qDebug() << "keeping the proxy: apiHttpStatus=" << apiHttpStatus;
return false;
}
if (apiHttpStatus == httpStatusCodeUnprocessableEntity) {
const bool bypass = apiErrorMessage != unprocessableSubscriptionMessage;
qDebug() << "apiHttpStatus=" << apiHttpStatus << "bypass=" << bypass
<< "(decided by the api message, which is not printed)";
return bypass;
return apiErrorMessage != unprocessableSubscriptionMessage;
}
if (replyError != QNetworkReply::NetworkError::NoError) {
qDebug() << "bypassing the proxy on a reply error:" << replyError << "apiHttpStatus=" << apiHttpStatus;
qDebug() << replyError;
return true;
}
qDebug() << "keeping the proxy: nothing matched, apiHttpStatus=" << apiHttpStatus << "replyError=" << replyError;
return false;
}
@@ -619,12 +566,10 @@ void GatewayController::bypassProxy(const QString &endpoint, const QString &serv
}
void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
const QString &proxyUrlsCacheKey, std::function<void(const QStringList &)> onComplete)
std::function<void(const QStringList &)> onComplete)
{
const QByteArray cachedProxyUrlsEncrypted = m_appSettingsRepository->readGatewayProxyUrls(proxyUrlsCacheKey);
if (currentProxyStorageIndex >= proxyStorageUrls.size()) {
onComplete(shuffledProxyUrls(readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment)));
onComplete({});
return;
}
@@ -637,17 +582,33 @@ 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, proxyUrlsCacheKey, onComplete, reply]() {
connect(reply, &QNetworkReply::finished, this, [this, proxyStorageUrls, currentProxyStorageIndex, onComplete, reply]() {
if (reply->error() == QNetworkReply::NoError) {
QByteArray encrypted = reply->readAll();
reply->deleteLater();
QByteArray responseBody;
if (!decryptProxyUrlsPayload(encrypted, m_isDevEnvironment, 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();
qCritical() << "error decrypting payload";
QMetaObject::invokeMethod(
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, proxyUrlsCacheKey, onComplete); }, Qt::QueuedConnection);
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
return;
}
@@ -655,9 +616,13 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
QStringList endpoints;
for (const QJsonValue &endpoint : endpointsArray)
endpoints.push_back(endpoint.toString());
m_appSettingsRepository->writeGatewayProxyUrls(proxyUrlsCacheKey, encrypted);
onComplete(shuffledProxyUrls(endpoints));
QStringList shuffled = endpoints;
std::random_device randomDevice;
std::mt19937 generator(randomDevice());
std::shuffle(shuffled.begin(), shuffled.end(), generator);
onComplete(shuffled);
return;
}
@@ -666,7 +631,7 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
qDebug() << "go to the next storage endpoint";
reply->deleteLater();
QMetaObject::invokeMethod(
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, proxyUrlsCacheKey, onComplete); }, Qt::QueuedConnection);
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
});
}

View File

@@ -16,16 +16,13 @@
#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, SecureAppSettingsRepository *appSettingsRepository,
QObject *parent = nullptr);
const bool isStrictKillSwitchEnabled, 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);
@@ -58,7 +55,7 @@ private:
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction);
void getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
const QString &proxyUrlsCacheKey, std::function<void(const QStringList &)> onComplete);
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,
@@ -68,7 +65,6 @@ private:
QString m_gatewayEndpoint;
bool m_isDevEnvironment = false;
bool m_isStrictKillSwitchEnabled = false;
SecureAppSettingsRepository *m_appSettingsRepository = nullptr;
inline static QString m_proxyUrl;
};

View File

@@ -1,7 +1,6 @@
#include "ipSplitTunnelingController.h"
#include "core/utils/networkUtilities.h"
#include <QJsonObject>
#include <QDebug>
IpSplitTunnelingController::IpSplitTunnelingController(SecureAppSettingsRepository* appSettingsRepository, QObject* parent)
: QObject(parent),
@@ -15,56 +14,47 @@ IpSplitTunnelingController::IpSplitTunnelingController(SecureAppSettingsReposito
fillSites();
}
bool IpSplitTunnelingController::addSiteInternal(const QString &hostname, const QStringList &ips)
bool IpSplitTunnelingController::addSiteInternal(const QString &hostname, const QString &ip)
{
QVariantMap existing = m_appSettingsRepository->vpnSites(m_currentRouteMode);
if (existing.contains(hostname) && ips.isEmpty()) {
if (existing.contains(hostname) && ip.isEmpty()) {
return false;
}
for (int i = 0; i < m_sites.size(); i++) {
if (m_sites[i].first == hostname) {
bool changed = false;
for (const QString &ip : ips) {
if (!ip.isEmpty() && !m_sites[i].second.contains(ip)) {
m_sites[i].second.append(ip);
changed = true;
}
}
if (!changed) {
return false;
}
m_appSettingsRepository->addVpnSite(m_currentRouteMode, hostname, ips);
if (m_sites[i].first == hostname && (m_sites[i].second.isEmpty() && !ip.isEmpty())) {
m_sites[i].second = ip;
m_appSettingsRepository->addVpnSite(m_currentRouteMode, hostname, ip);
return true;
} else if (m_sites[i].first == hostname && (m_sites[i].second == ip)) {
return false;
}
}
m_sites.append(qMakePair(hostname, ips));
m_appSettingsRepository->addVpnSite(m_currentRouteMode, hostname, ips);
m_sites.append(qMakePair(hostname, ip));
m_appSettingsRepository->addVpnSite(m_currentRouteMode, hostname, ip);
return true;
}
void IpSplitTunnelingController::addSites(const QMap<QString, QStringList> &sites, bool replaceExisting)
void IpSplitTunnelingController::addSites(const QMap<QString, QString> &sites, bool replaceExisting)
{
if (replaceExisting) {
m_sites.clear();
}
for (auto it = sites.constBegin(); it != sites.constEnd(); ++it) {
const QString &hostname = it.key();
const QStringList &ips = it.value();
const QString &ip = it.value();
bool found = false;
for (int i = 0; i < m_sites.size(); i++) {
if (m_sites[i].first == hostname) {
for (const QString &ip : ips) {
if (!ip.isEmpty() && !m_sites[i].second.contains(ip)) {
m_sites[i].second.append(ip);
}
if (!ip.isEmpty()) {
m_sites[i].second = ip;
}
found = true;
break;
}
}
if (!found) {
m_sites.append(qMakePair(hostname, ips));
m_sites.append(qMakePair(hostname, ip));
}
}
if (replaceExisting) {
@@ -82,11 +72,11 @@ bool IpSplitTunnelingController::addSite(const QString &hostname)
}
if (NetworkUtilities::ipAddressWithSubnetRegExp().exactMatch(normalizedHostname)) {
processSite(normalizedHostname, {});
processSite(normalizedHostname, "");
return true;
}
if (addSiteInternal(normalizedHostname, {})) {
if (addSiteInternal(normalizedHostname, "")) {
QHostInfo::lookupHost(normalizedHostname, this, SLOT(onHostResolved(QHostInfo)));
return true;
}
@@ -134,7 +124,7 @@ bool IpSplitTunnelingController::isSplitTunnelingEnabled() const
return m_appSettingsRepository->isSitesSplitTunnelingEnabled();
}
QVector<QPair<QString, QStringList>> IpSplitTunnelingController::getCurrentSites() const
QVector<QPair<QString, QString>> IpSplitTunnelingController::getCurrentSites() const
{
return m_sites;
}
@@ -144,7 +134,7 @@ void IpSplitTunnelingController::fillSites()
QVariantMap sitesMap = m_appSettingsRepository->vpnSites(m_currentRouteMode);
m_sites.clear();
for (auto it = sitesMap.begin(); it != sitesMap.end(); ++it) {
m_sites.append(qMakePair(it.key(), SecureAppSettingsRepository::siteIpList(it.value())));
m_sites.append(qMakePair(it.key(), it.value().toString()));
}
}
@@ -154,13 +144,8 @@ QString IpSplitTunnelingController::normalizeHostname(const QString &hostname) c
normalized.replace("https://", "");
normalized.replace("http://", "");
normalized.replace("ftp://", "");
if (NetworkUtilities::ipAddressWithSubnetRegExp().exactMatch(normalized)) {
return normalized;
}
const QStringList parts = normalized.split("/", Qt::SkipEmptyParts);
return parts.isEmpty() ? QString() : parts.first();
normalized = normalized.split("/", Qt::SkipEmptyParts).first();
return normalized;
}
bool IpSplitTunnelingController::validateHostname(const QString &hostname) const
@@ -179,40 +164,29 @@ void IpSplitTunnelingController::onHostResolved(const QHostInfo &hostInfo)
{
const QList<QHostAddress> &addresses = hostInfo.addresses();
QString hostname = hostInfo.hostName();
QStringList allIpv4;
for (const QHostAddress &addr : addresses) {
if (addr.protocol() == QAbstractSocket::NetworkLayerProtocol::IPv4Protocol) {
allIpv4.append(addr.toString());
}
}
allIpv4.removeDuplicates();
qDebug() << "[SplitTunneling] Host resolved:" << hostname
<< "-> adding all IPv4 addresses to list:" << allIpv4;
if (!allIpv4.isEmpty()) {
processSiteAfterResolve(hostname, allIpv4);
}
}
void IpSplitTunnelingController::processSiteAfterResolve(const QString &hostname, const QStringList &ips)
{
for (int i = 0; i < m_sites.size(); i++) {
if (m_sites[i].first == hostname) {
for (const QString &ip : ips) {
if (!ip.isEmpty() && !m_sites[i].second.contains(ip)) {
m_sites[i].second.append(ip);
}
}
processSiteAfterResolve(hostname, addr.toString());
break;
}
}
m_appSettingsRepository->addVpnSite(m_currentRouteMode, hostname, ips);
}
void IpSplitTunnelingController::processSite(const QString &hostname, const QStringList &ips)
void IpSplitTunnelingController::processSiteAfterResolve(const QString &hostname, const QString &ip)
{
addSiteInternal(hostname, ips);
for (int i = 0; i < m_sites.size(); i++) {
if (m_sites[i].first == hostname && m_sites[i].second.isEmpty()) {
m_sites[i].second = ip;
m_appSettingsRepository->addVpnSite(m_currentRouteMode, hostname, ip);
break;
}
}
}
void IpSplitTunnelingController::processSite(const QString &hostname, const QString &ip)
{
addSiteInternal(hostname, ip);
}
bool IpSplitTunnelingController::importSitesFromJson(const QByteArray& jsonData, bool replaceExisting, QString &errorMessage)
@@ -231,25 +205,12 @@ bool IpSplitTunnelingController::importSitesFromJson(const QByteArray& jsonData,
}
QJsonArray jsonArray = jsonDocument.array();
QMap<QString, QStringList> sites;
QMap<QString, QString> sites;
for (auto jsonValue : jsonArray) {
QJsonObject jsonObject = jsonValue.toObject();
QString hostname = jsonObject.value("hostname").toString("");
QStringList ips;
if (jsonObject.value("ips").isArray()) {
const QJsonArray ipsArray = jsonObject.value("ips").toArray();
for (const auto &ipValue : ipsArray) {
ips.append(ipValue.toString());
}
}
const QString singleIp = jsonObject.value("ip").toString("");
if (!singleIp.isEmpty()) {
ips.append(singleIp);
}
ips.removeAll(QString());
ips.removeDuplicates();
QString ip = jsonObject.value("ip").toString("");
QString normalizedHostname = normalizeHostname(hostname);
@@ -258,7 +219,7 @@ bool IpSplitTunnelingController::importSitesFromJson(const QByteArray& jsonData,
continue;
}
sites.insert(normalizedHostname, ips);
sites.insert(normalizedHostname, ip);
}
addSites(sites, replaceExisting);
@@ -268,21 +229,13 @@ bool IpSplitTunnelingController::importSitesFromJson(const QByteArray& jsonData,
QByteArray IpSplitTunnelingController::exportSitesToJson() const
{
QVector<QPair<QString, QStringList>> sites = getCurrentSites();
QVector<QPair<QString, QString>> sites = getCurrentSites();
QJsonArray jsonArray;
for (const auto &site : sites) {
QJsonObject jsonObject;
jsonObject["hostname"] = site.first;
QJsonArray ipsArray;
for (const QString &ip : site.second) {
ipsArray.append(ip);
}
jsonObject["ips"] = ipsArray;
// Keep the legacy "ip" field (first address) for backward compatibility.
jsonObject["ip"] = site.second.isEmpty() ? QString() : site.second.first();
jsonObject["ip"] = site.second;
jsonArray.append(jsonObject);
}

View File

@@ -25,7 +25,7 @@ public:
explicit IpSplitTunnelingController(SecureAppSettingsRepository* appSettingsRepository, QObject* parent = nullptr);
bool addSite(const QString &hostname);
void addSites(const QMap<QString, QStringList> &sites, bool replaceExisting);
void addSites(const QMap<QString, QString> &sites, bool replaceExisting);
bool removeSite(const QString &hostname);
void removeSites();
void setRouteMode(RouteMode routeMode);
@@ -33,7 +33,7 @@ public:
RouteMode getRouteMode() const;
bool isSplitTunnelingEnabled() const;
QVector<QPair<QString, QStringList>> getCurrentSites() const;
QVector<QPair<QString, QString>> getCurrentSites() const;
bool importSitesFromJson(const QByteArray& jsonData, bool replaceExisting, QString &errorMessage);
QByteArray exportSitesToJson() const;
@@ -43,15 +43,15 @@ private slots:
private:
void fillSites();
bool addSiteInternal(const QString &hostname, const QStringList &ips);
bool addSiteInternal(const QString &hostname, const QString &ip);
QString normalizeHostname(const QString &hostname) const;
bool validateHostname(const QString &hostname) const;
void processSiteAfterResolve(const QString &hostname, const QStringList &ips);
void processSite(const QString &hostname, const QStringList &ips);
void processSiteAfterResolve(const QString &hostname, const QString &ip);
void processSite(const QString &hostname, const QString &ip);
SecureAppSettingsRepository* m_appSettingsRepository;
RouteMode m_currentRouteMode;
QVector<QPair<QString, QStringList>> m_sites;
QVector<QPair<QString, QString>> m_sites;
};
#endif // IPSPLITTUNNELINGCONTROLLER_H

View File

@@ -14,7 +14,6 @@
#include "core/models/selfhosted/selfHostedAdminServerConfig.h"
#include "core/models/containerConfig.h"
#include "core/models/protocolConfig.h"
#include "version.h"
using namespace amnezia;
@@ -221,10 +220,7 @@ ExportController::ExportResult ExportController::generateWireGuardConfig(const Q
result.config.append(line + "\n");
}
const QString qr = qrCodeUtils::generatePlainQrCodeImage(result.config.toUtf8());
if (!qr.isEmpty()) {
result.qrCodes << qr;
}
result.qrCodes << generateSingleQrCode(result.config.toUtf8());
return result;
}
@@ -255,10 +251,7 @@ ExportController::ExportResult ExportController::generateAwgConfig(const QString
result.config.append(line + "\n");
}
const QString qr = qrCodeUtils::generatePlainQrCodeImage(result.config.toUtf8());
if (!qr.isEmpty()) {
result.qrCodes << qr;
}
result.qrCodes << generateSingleQrCode(result.config.toUtf8());
return result;
}
@@ -330,21 +323,9 @@ ExportController::ExportResult ExportController::generateXrayConfig(const QStrin
vlessServer.shortId = realitySettings.value(amnezia::protocols::xray::shortId).toString();
vlessServer.fingerprint = realitySettings.value(amnezia::protocols::xray::fingerprint).toString("chrome");
vlessServer.spiderX = realitySettings.value(amnezia::protocols::xray::spiderX).toString("");
} else if (vlessServer.security == "tls") {
QJsonObject tlsSettings = streamSettings.value("tlsSettings").toObject();
vlessServer.serverName = tlsSettings.value(amnezia::protocols::xray::serverName).toString();
vlessServer.fingerprint = tlsSettings.value(amnezia::protocols::xray::fingerprint).toString();
// alpn: serialize array back to comma-separated for VLESS URI
QJsonArray alpnArr = tlsSettings.value("alpn").toArray();
QStringList alpnList;
for (const QJsonValue &v : alpnArr) {
alpnList << v.toString();
}
// alpn goes into vless URI query param — handled by Serialize via serverName/alpn fields
// VlessServerObject doesn't have alpn field, so we embed in serverName if needed
}
result.nativeConfigString = amnezia::serialization::vless::Serialize(vlessServer, APPLICATION_NAME);
result.nativeConfigString = amnezia::serialization::vless::Serialize(vlessServer, "AmneziaVPN");
return result;
}
@@ -376,3 +357,9 @@ QList<QString> ExportController::generateQrCodesFromConfig(const QByteArray &dat
{
return qrCodeUtils::generateQrCodeImageSeries(data);
}
QString ExportController::generateSingleQrCode(const QByteArray &data)
{
auto qr = qrCodeUtils::generateQrCode(data);
return qrCodeUtils::svgToBase64(QString::fromStdString(toSvgString(qr, 1)));
}

View File

@@ -48,7 +48,6 @@ signals:
void appendClientRequested(const QString &serverId, const QString &clientId, const QString &clientName, DockerContainer container);
void updateClientsRequested(const QString &serverId, DockerContainer container);
void revokeClientRequested(const QString &serverId, int row, DockerContainer container);
void revokeFinished(ErrorCode errorCode);
void renameClientRequested(const QString &serverId, int row, const QString &clientName, DockerContainer container);
public slots:
@@ -69,6 +68,7 @@ private:
QString generateVpnUrl(const QByteArray &compressedConfig);
QList<QString> generateQrCodesFromConfig(const QByteArray &data);
QString generateSingleQrCode(const QByteArray &data);
SecureServersRepository* m_serversRepository;
SecureAppSettingsRepository* m_appSettingsRepository;

View File

@@ -486,7 +486,7 @@ QJsonObject ImportController::extractOpenVpnConfig(const QString &data) const
QJsonObject config;
config[configKey::containers] = arr;
config[configKey::defaultContainer] = configKey::amneziaOpenvpn;
config[configKey::description] = m_serversRepository->nextAvailableServerName();
config[configKey::description] = m_appSettingsRepository->nextAvailableServerName();
const static QRegularExpression dnsRegExp("dhcp-option DNS (\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b)");
QRegularExpressionMatchIterator dnsMatch = dnsRegExp.globalMatch(data);
@@ -511,10 +511,9 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data, Config
if (trimmedLine.startsWith("[") && trimmedLine.endsWith("]")) {
continue;
} else {
const qsizetype separatorIndex = trimmedLine.indexOf('=');
if (separatorIndex > 0) {
configMap[trimmedLine.left(separatorIndex).trimmed()] =
trimmedLine.mid(separatorIndex + 1).trimmed();
QStringList parts = trimmedLine.split(" = ");
if (parts.count() == 2) {
configMap[parts.at(0).trimmed()] = parts.at(1).trimmed();
}
}
}
@@ -567,26 +566,53 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data, Config
lastConfig[configKey::persistentKeepAlive] = configMap.value(protocols::wireguard::PersistentKeepalive);
}
const QStringList allowedIps = configMap.value(protocols::wireguard::AllowedIPs).split(
QRegularExpression("\\s*,\\s*"), Qt::SkipEmptyParts);
QJsonArray allowedIpsJsonArray = QJsonArray::fromStringList(allowedIps);
QJsonArray allowedIpsJsonArray = QJsonArray::fromStringList(
configMap.value(protocols::wireguard::AllowedIPs).split(", "));
lastConfig[configKey::allowedIps] = allowedIpsJsonArray;
QString protocolName = configKey::wireguard;
QString protocolVersion;
ConfigTypes detectedType = ConfigTypes::WireGuard;
const QStringList awgProtocolKeys = configKey::awgProtocolKeys();
const QStringList requiredJunkFields = { configKey::junkPacketCount, configKey::junkPacketMinSize,
configKey::junkPacketMaxSize, configKey::initPacketJunkSize,
configKey::responsePacketJunkSize, configKey::initPacketMagicHeader,
configKey::responsePacketMagicHeader, configKey::underloadPacketMagicHeader,
configKey::transportPacketMagicHeader };
bool hasAwgKeys = std::any_of(awgProtocolKeys.begin(), awgProtocolKeys.end(),
[&configMap](const QString &field) { return !configMap.value(field).isEmpty(); });
if (hasAwgKeys) {
for (const QString &key : awgProtocolKeys) {
if (!configMap.value(key).isEmpty()) {
lastConfig[key] = configMap.value(key);
const QStringList optionalJunkFields = { configKey::cookieReplyPacketJunkSize,
configKey::transportPacketJunkSize,
configKey::specialJunk1, configKey::specialJunk2, configKey::specialJunk3,
configKey::specialJunk4, configKey::specialJunk5
};
bool hasAllRequiredFields = std::all_of(requiredJunkFields.begin(), requiredJunkFields.end(),
[&configMap](const QString &field) { return !configMap.value(field).isEmpty(); });
if (hasAllRequiredFields) {
for (const QString &field : requiredJunkFields) {
lastConfig[field] = configMap.value(field);
}
for (const QString &field : optionalJunkFields) {
if (!configMap.value(field).isEmpty()) {
lastConfig[field] = configMap.value(field);
}
}
bool hasCookieReplyPacketJunkSize = !configMap.value(configKey::cookieReplyPacketJunkSize).isEmpty();
bool hasTransportPacketJunkSize = !configMap.value(configKey::transportPacketJunkSize).isEmpty();
bool hasSpecialJunk = !configMap.value(configKey::specialJunk1).isEmpty() ||
!configMap.value(configKey::specialJunk2).isEmpty() ||
!configMap.value(configKey::specialJunk3).isEmpty() ||
!configMap.value(configKey::specialJunk4).isEmpty() ||
!configMap.value(configKey::specialJunk5).isEmpty();
if (hasCookieReplyPacketJunkSize && hasTransportPacketJunkSize) {
protocolVersion = "2";
} else if (hasSpecialJunk && !hasCookieReplyPacketJunkSize && !hasTransportPacketJunkSize) {
protocolVersion = "1.5";
}
protocolName = configKey::awg;
detectedType = ConfigTypes::Awg;
}
@@ -604,6 +630,9 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data, Config
wireguardConfig[configKey::isThirdPartyConfig] = true;
wireguardConfig[configKey::port] = port;
wireguardConfig[configKey::transportProto] = protocols::openvpn::defaultTransportProto;
if (protocolName == configKey::awg && !protocolVersion.isEmpty()) {
wireguardConfig[configKey::protocolVersion] = protocolVersion;
}
QJsonObject containers;
QString containerName = (protocolName == configKey::awg) ? configKey::amneziaAwg : configKey::amneziaWireguard;
@@ -616,7 +645,7 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data, Config
QJsonObject config;
config[configKey::containers] = arr;
config[configKey::defaultContainer] = containerName;
config[configKey::description] = m_serversRepository->nextAvailableServerName();
config[configKey::description] = m_appSettingsRepository->nextAvailableServerName();
const static QRegularExpression dnsRegExp(
"DNS = "
@@ -637,24 +666,11 @@ QJsonObject ImportController::extractXrayConfig(const QString &data, ConfigTypes
{
QJsonParseError parserErr;
QJsonDocument jsonConf = QJsonDocument::fromJson(data.toLocal8Bit(), &parserErr);
if (parserErr.error != QJsonParseError::NoError || !jsonConf.isObject()) {
qDebug() << "Xray config JSON parse failed:" << parserErr.errorString();
return QJsonObject();
}
const QJsonObject parsedConfig = jsonConf.object();
if (!parsedConfig.value(protocols::xray::inbounds).isArray()
|| !parsedConfig.value(protocols::xray::outbounds).isArray()) {
qDebug() << "Xray config is missing inbounds or outbounds";
return QJsonObject();
}
const QString serializedConfig = QString::fromUtf8(jsonConf.toJson());
QJsonObject xrayVpnConfig;
xrayVpnConfig[configKey::config] = serializedConfig;
xrayVpnConfig[configKey::config] = jsonConf.toJson().constData();
QJsonObject lastConfig;
lastConfig[configKey::lastConfig] = serializedConfig;
lastConfig[configKey::lastConfig] = jsonConf.toJson().constData();
lastConfig[configKey::isThirdPartyConfig] = true;
QJsonObject containers;
@@ -683,7 +699,7 @@ QJsonObject ImportController::extractXrayConfig(const QString &data, ConfigTypes
? configKey::amneziaSsxray
: configKey::amneziaXray;
if (description.isEmpty()) {
config[configKey::description] = m_serversRepository->nextAvailableServerName();
config[configKey::description] = m_appSettingsRepository->nextAvailableServerName();
} else {
config[configKey::description] = description;
}

File diff suppressed because it is too large Load Diff

View File

@@ -16,7 +16,6 @@
#include "core/models/containerConfig.h"
#include "core/repositories/secureServersRepository.h"
#include "core/repositories/secureAppSettingsRepository.h"
#include "core/installers/mtProxyInstaller.h"
class SshSession;
class InstallerBase;
@@ -34,27 +33,12 @@ public:
~InstallController();
ErrorCode setupContainer(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config, bool isUpdate = false);
// Updates server-side container settings (admin self-hosted only): reconfigures the container over SSH.
ErrorCode updateServerConfig(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig, ContainerConfig &newConfig);
// Updates client-local settings only: rewrites the stored container config for any self-hosted/native server. No SSH.
ErrorCode updateClientConfig(const QString &serverId, DockerContainer container, ContainerConfig &newConfig);
ErrorCode updateContainer(const QString &serverId, DockerContainer container, const ContainerConfig &oldConfig, ContainerConfig &newConfig);
ErrorCode rebootServer(const QString &serverId);
ErrorCode removeAllContainers(const QString &serverId);
ErrorCode removeContainer(const QString &serverId, DockerContainer container);
ErrorCode setDockerContainerEnabledState(const QString &serverId, DockerContainer container, bool enabled);
/// statusOut: 0 = not deployed, 1 = running, 2 = stopped, 3 = error
ErrorCode queryDockerContainerStatus(const QString &serverId, DockerContainer container, int &statusOut);
ErrorCode queryMtProxyDiagnostics(const QString &serverId, DockerContainer container, int listenPort,
MtProxyContainerDiagnostics &out);
QString fetchDockerContainerSecret(const QString &serverId, DockerContainer container);
ContainerConfig generateConfig(DockerContainer container, int port, TransportProto transportProto);
ErrorCode getAlreadyInstalledContainers(const ServerCredentials &credentials, QMap<DockerContainer, ContainerConfig> &installedContainers, SshSession &sshSession);
@@ -69,8 +53,7 @@ public:
bool isUpdateDockerContainerRequired(DockerContainer container, const ContainerConfig &oldConfig, const ContainerConfig &newConfig);
ErrorCode checkSshConnection(ServerCredentials &credentials, QString &output,
std::function<QString()> passphraseCallback = nullptr);
ErrorCode checkSshConnection(const ServerCredentials &credentials, QString &output, std::function<QString()> passphraseCallback = nullptr);
bool isServerAlreadyExists(const ServerCredentials &credentials, int &existingServerIndex);
@@ -104,11 +87,6 @@ private:
ErrorCode configureContainerWorker(const ServerCredentials &credentials, DockerContainer container, ContainerConfig &config, SshSession &sshSession);
ErrorCode startupContainerWorker(const ServerCredentials &credentials, DockerContainer container, const ContainerConfig &config, SshSession &sshSession);
ErrorCode readXrayStateBeforeVolumeMigration(const ServerCredentials &credentials, DockerContainer container,
SshSession &sshSession, QMap<QString, QString> &outFiles);
ErrorCode restoreXrayStateIntoDataVolume(const ServerCredentials &credentials, DockerContainer container,
SshSession &sshSession, const QMap<QString, QString> &files);
ErrorCode isServerPortBusy(const ServerCredentials &credentials, DockerContainer container, const ContainerConfig &config, SshSession &sshSession);
ErrorCode isUserInSudo(const ServerCredentials &credentials, SshSession &sshSession);
ErrorCode isServerDpkgBusy(const ServerCredentials &credentials, SshSession &sshSession);
@@ -131,7 +109,7 @@ private:
SecureServersRepository* m_serversRepository;
SecureAppSettingsRepository* m_appSettingsRepository;
bool m_cancelInstallation = false;
#ifndef Q_OS_IOS
QList<QSharedPointer<QProcess>> m_sftpMountProcesses;
#endif

View File

@@ -2,7 +2,6 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QDateTime>
#include "core/utils/containerEnum.h"
@@ -56,34 +55,6 @@ int UsersController::clientIndexById(const QString &clientId, const QJsonArray &
return -1;
}
QString UsersController::readXrayVolumeUuid(const DockerContainer container, const ServerCredentials &credentials,
SshSession *sshSession) const
{
ErrorCode error = ErrorCode::NoError;
const QString uuid = QString::fromUtf8(sshSession->getTextFileFromContainer(
container, credentials, QString::fromLatin1(amnezia::protocols::xray::uuidPath), error));
if (error != ErrorCode::NoError) {
logger.warning() << "readXrayVolumeUuid: failed, error=" << static_cast<int>(error);
return {};
}
return uuid.trimmed();
}
int UsersController::stripXrayVolumeUuidFromTable(QJsonArray &clientsTable, const QString &volumeUuid)
{
if (volumeUuid.isEmpty()) {
return 0;
}
int removed = 0;
for (int i = clientsTable.size() - 1; i >= 0; --i) {
if (clientsTable.at(i).toObject().value(configKey::clientId).toString() == volumeUuid) {
clientsTable.removeAt(i);
++removed;
}
}
return removed;
}
void UsersController::migration(const QByteArray &clientsTableString, QJsonArray &clientsTable)
{
QJsonObject clientsTableObj = QJsonDocument::fromJson(clientsTableString).object();
@@ -293,7 +264,6 @@ ErrorCode UsersController::getXrayClients(const DockerContainer container, const
}
const QJsonArray clients = settings[protocols::xray::clients].toArray();
const QString xrayDefaultUuid = readXrayVolumeUuid(container, credentials, sshSession);
for (const auto &clientValue : clients) {
const QJsonObject clientObj = clientValue.toObject();
if (!clientObj.contains(protocols::xray::id)) {
@@ -301,6 +271,9 @@ ErrorCode UsersController::getXrayClients(const DockerContainer container, const
continue;
}
QString clientId = clientObj[protocols::xray::id].toString();
QString xrayDefaultUuid = sshSession->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::uuidPath, error);
xrayDefaultUuid.replace("\n", "");
if (!isClientExists(clientId, clientsTable) && clientId != xrayDefaultUuid) {
QJsonObject client;
@@ -324,14 +297,10 @@ ErrorCode UsersController::updateClients(const QString &serverId, const DockerCo
SshSession sshSession;
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
if (!adminConfig.has_value()) {
logger.error() << "updateClients: no admin config for this server, container="
<< ContainerUtils::containerTypeToString(container);
return ErrorCode::InternalError;
}
ServerCredentials credentials = adminConfig->credentials();
if (!credentials.isValid()) {
logger.error() << "updateClients: credentials are not valid, container="
<< ContainerUtils::containerTypeToString(container);
return ErrorCode::InternalError;
}
@@ -344,8 +313,7 @@ ErrorCode UsersController::updateClients(const QString &serverId, const DockerCo
const QByteArray clientsTableString = sshSession.getTextFileFromContainer(container, credentials, clientsTableFile, error);
if (error != ErrorCode::NoError) {
logger.error() << "updateClients: failed to read the clientsTable file, error=" << static_cast<int>(error)
<< "container=" << ContainerUtils::containerTypeToString(container);
logger.error() << "Failed to get the clientsTable file from the server";
emit clientsUpdated(QJsonArray());
return error;
}
@@ -353,10 +321,6 @@ ErrorCode UsersController::updateClients(const QString &serverId, const DockerCo
m_clientsTable = QJsonDocument::fromJson(clientsTableString).array();
if (m_clientsTable.isEmpty()) {
logger.info() << "updateClients: the clientsTable is empty, rebuilding it from the server, readBytes="
<< clientsTableString.size()
<< (clientsTableString.trimmed().isEmpty() ? ", the file was empty"
: ", the file had content but did not parse as an array");
migration(clientsTableString, m_clientsTable);
int count = 0;
@@ -369,8 +333,6 @@ ErrorCode UsersController::updateClients(const QString &serverId, const DockerCo
error = getXrayClients(container, credentials, &sshSession, count, m_clientsTable);
}
if (error != ErrorCode::NoError) {
logger.error() << "updateClients: failed to rebuild the client list from the server, error="
<< static_cast<int>(error) << "container=" << ContainerUtils::containerTypeToString(container);
emit clientsUpdated(QJsonArray());
return error;
}
@@ -379,27 +341,7 @@ ErrorCode UsersController::updateClients(const QString &serverId, const DockerCo
if (clientsTableString != newClientsTableString) {
error = sshSession.uploadTextFileToContainer(container, credentials, newClientsTableString, clientsTableFile);
if (error != ErrorCode::NoError) {
logger.error() << "updateClients: failed to write back the rebuilt clientsTable, error="
<< static_cast<int>(error) << "container=" << ContainerUtils::containerTypeToString(container)
<< "rows=" << m_clientsTable.size();
logger.warning() << "updateClients: the app now holds a client list the server does not have;"
<< "this error is returned but every caller discards it";
}
}
}
if (container == DockerContainer::Xray) {
const QString volumeUuid = readXrayVolumeUuid(container, credentials, &sshSession);
const int stripped = stripXrayVolumeUuidFromTable(m_clientsTable, volumeUuid);
if (stripped > 0) {
logger.info() << "updateClients: removed the volume uuid from the user list, rows="
<< stripped;
const QByteArray strippedTable = QJsonDocument(m_clientsTable).toJson();
const ErrorCode writeError =
sshSession.uploadTextFileToContainer(container, credentials, strippedTable, clientsTableFile);
if (writeError != ErrorCode::NoError) {
logger.error() << "updateClients: failed to write the user list without the volume uuid, error="
<< static_cast<int>(writeError);
logger.error() << "Failed to upload the clientsTable file to the server";
}
}
}
@@ -451,32 +393,18 @@ ErrorCode UsersController::appendClient(const QString &serverId, const QString &
SshSession sshSession;
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
if (!adminConfig.has_value()) {
logger.error() << "appendClient: no admin config for this server, container="
<< ContainerUtils::containerTypeToString(container);
return ErrorCode::InternalError;
}
ServerCredentials credentials = adminConfig->credentials();
if (!credentials.isValid()) {
logger.error() << "appendClient: credentials are not valid, container="
<< ContainerUtils::containerTypeToString(container);
return ErrorCode::InternalError;
}
error = updateClients(serverId, container);
if (error != ErrorCode::NoError) {
logger.error() << "appendClient: could not read the current client list, error=" << static_cast<int>(error)
<< ", the new account will not appear in the user list";
return error;
}
if (container == DockerContainer::Xray) {
const QString volumeUuid = readXrayVolumeUuid(container, credentials, &sshSession);
if (!volumeUuid.isEmpty() && clientId == volumeUuid) {
logger.info() << "appendClient: skipped the volume uuid, it is not listed in Share Users";
return ErrorCode::NoError;
}
}
int existingIndex = clientIndexById(clientId, m_clientsTable);
if (existingIndex >= 0) {
return renameClient(serverId, existingIndex, clientName, container, true);
@@ -502,11 +430,7 @@ ErrorCode UsersController::appendClient(const QString &serverId, const QString &
error = sshSession.uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile);
if (error != ErrorCode::NoError) {
logger.error() << "appendClient: failed to upload the clientsTable, error=" << static_cast<int>(error)
<< "container=" << ContainerUtils::containerTypeToString(container)
<< "rows=" << m_clientsTable.size();
logger.warning() << "appendClient: the account exists on the server but is missing from the user list;"
<< "the caller discards this error, so nothing is reported to the user";
logger.error() << "Failed to upload the clientsTable file to the server";
return error;
}
@@ -553,9 +477,7 @@ ErrorCode UsersController::renameClient(const QString &serverId, const int row,
ErrorCode error = sshSession.uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile);
if (error != ErrorCode::NoError) {
logger.error() << "renameClient: failed to upload the clientsTable, error=" << static_cast<int>(error)
<< "container=" << ContainerUtils::containerTypeToString(container)
<< "rows=" << m_clientsTable.size();
logger.error() << "Failed to upload the clientsTable file to the server";
return error;
}
@@ -600,9 +522,7 @@ ErrorCode UsersController::revokeOpenVpn(const int row, const DockerContainer co
clientsTableFile = clientsTableFile.arg(ContainerUtils::containerTypeToString(DockerContainer::OpenVpn));
error = sshSession->uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile);
if (error != ErrorCode::NoError) {
logger.error() << "revokeOpenVpn: failed to upload the clientsTable, error=" << static_cast<int>(error)
<< "container=" << ContainerUtils::containerTypeToString(container)
<< "rows=" << clientsTable.size();
logger.error() << "Failed to upload the clientsTable file to the server";
return error;
}
@@ -662,9 +582,7 @@ ErrorCode UsersController::revokeWireGuard(const int row, const DockerContainer
}
error = sshSession->uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile);
if (error != ErrorCode::NoError) {
logger.error() << "revokeWireGuard: failed to upload the clientsTable, error=" << static_cast<int>(error)
<< "container=" << ContainerUtils::containerTypeToString(container)
<< "rows=" << clientsTable.size();
logger.error() << "Failed to upload the clientsTable file to the server";
return error;
}
@@ -697,24 +615,6 @@ ErrorCode UsersController::revokeXray(const int row,
ErrorCode error = ErrorCode::NoError;
auto client = clientsTable.at(row).toObject();
QString clientId = client.value(configKey::clientId).toString();
const QString volumeUuid = readXrayVolumeUuid(container, credentials, sshSession);
if (!volumeUuid.isEmpty() && clientId == volumeUuid) {
logger.info() << "revokeXray: refused, this is the volume uuid";
clientsTable.removeAt(row);
const QByteArray clientsTableString = QJsonDocument(clientsTable).toJson();
const QString clientsTableFile = QString("/opt/amnezia/%1/clientsTable")
.arg(ContainerUtils::containerTypeToString(container));
error = sshSession->uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile);
if (error != ErrorCode::NoError) {
logger.error() << "revokeXray: refused the volume uuid but failed to drop it from the user list, error="
<< static_cast<int>(error);
}
return ErrorCode::NoError;
}
const QString serverConfigPath = amnezia::protocols::xray::serverConfigPath;
const QString configString = sshSession->getTextFileFromContainer(container, credentials, serverConfigPath, error);
if (error != ErrorCode::NoError) {
@@ -728,6 +628,9 @@ ErrorCode UsersController::revokeXray(const int row,
return ErrorCode::InternalError;
}
auto client = clientsTable.at(row).toObject();
QString clientId = client.value(configKey::clientId).toString();
QJsonObject configObj = serverConfig.object();
if (!configObj.contains(protocols::xray::inbounds)) {
logger.error() << "Missing inbounds in xray config";
@@ -766,10 +669,6 @@ ErrorCode UsersController::revokeXray(const int row,
}
}
if (clients.isEmpty()) {
logger.warning() << "revokeXray: inbound left with zero clients";
}
settings[protocols::xray::clients] = clients;
inbound[protocols::xray::settings] = settings;
inbounds[0] = inbound;
@@ -794,16 +693,12 @@ ErrorCode UsersController::revokeXray(const int row,
error = sshSession->uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile);
if (error != ErrorCode::NoError) {
logger.error() << "revokeXray: failed to upload the clientsTable, error=" << static_cast<int>(error)
<< "container=" << ContainerUtils::containerTypeToString(container)
<< "rows=" << clientsTable.size();
logger.warning() << "revokeXray: the account is gone from the server config but still listed in the user table;"
<< "this error is then overwritten by the container restart below and never reaches the caller";
logger.error() << "Failed to upload the clientsTable file";
}
QString restartScript = QString("sudo docker restart $CONTAINER_NAME");
error = sshSession->runScript(
credentials,
credentials,
sshSession->replaceVars(restartScript, amnezia::genBaseVars(credentials, container, QString(), QString()))
);
if (error != ErrorCode::NoError) {
@@ -817,22 +712,16 @@ ErrorCode UsersController::revokeXray(const int row,
ErrorCode UsersController::revokeClient(const QString &serverId, const int index, const DockerContainer container)
{
if (index < 0 || index >= m_clientsTable.size()) {
logger.error() << "revokeClient: row" << index << "is outside the client table of" << m_clientsTable.size()
<< "rows, container=" << ContainerUtils::containerTypeToString(container);
return ErrorCode::InternalError;
}
SshSession sshSession;
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
if (!adminConfig.has_value()) {
logger.error() << "revokeClient: no admin config for this server, container="
<< ContainerUtils::containerTypeToString(container);
return ErrorCode::InternalError;
}
ServerCredentials credentials = adminConfig->credentials();
if (!credentials.isValid()) {
logger.error() << "revokeClient: credentials are not valid, container="
<< ContainerUtils::containerTypeToString(container);
return ErrorCode::InternalError;
}
@@ -869,17 +758,14 @@ ErrorCode UsersController::revokeClient(const QString &serverId, const int index
ContainerConfig containerCfg = adminConfig->containerConfig(container);
QString containerClientId = containerCfg.protocolConfig.clientId();
const bool isAdminMatch = !clientId.isEmpty() && !containerClientId.isEmpty() && containerClientId.contains(clientId);
if (isAdminMatch) {
if (!clientId.isEmpty() && !containerClientId.isEmpty() && containerClientId.contains(clientId)) {
emit adminConfigRevoked(serverId, container);
}
emit clientRevoked(index);
emit clientsUpdated(m_clientsTable);
}
emit clientsUpdated(m_clientsTable);
emit revokeFinished(errorCode);
return errorCode;
}
@@ -888,22 +774,16 @@ ErrorCode UsersController::revokeClient(const QString &serverId, const Container
SshSession sshSession;
auto adminConfig = m_serversRepository->selfHostedAdminConfig(serverId);
if (!adminConfig.has_value()) {
logger.error() << "revokeClient by config: no admin config for this server, container="
<< ContainerUtils::containerTypeToString(container);
return ErrorCode::InternalError;
}
ServerCredentials credentials = adminConfig->credentials();
if (!credentials.isValid()) {
logger.error() << "revokeClient by config: credentials are not valid, container="
<< ContainerUtils::containerTypeToString(container);
return ErrorCode::InternalError;
}
ErrorCode errorCode = ErrorCode::NoError;
errorCode = updateClients(serverId, container);
if (errorCode != ErrorCode::NoError) {
logger.error() << "revokeClient by config: could not read the current client list, error="
<< static_cast<int>(errorCode);
return errorCode;
}
@@ -929,9 +809,6 @@ ErrorCode UsersController::revokeClient(const QString &serverId, const Container
int row = clientIndexById(clientId, m_clientsTable);
if (row < 0) {
logger.warning() << "revokeClient by config: this account is not in the client table of"
<< m_clientsTable.size() << "rows, nothing was revoked and success is reported,"
<< "container=" << ContainerUtils::containerTypeToString(container);
return errorCode;
}

View File

@@ -37,7 +37,6 @@ signals:
void clientAdded(const QJsonObject &client);
void clientRenamed(int row, const QString &newName);
void clientRevoked(int row);
void revokeFinished(ErrorCode errorCode);
void adminConfigRevoked(const QString &serverId, DockerContainer container);
public slots:
@@ -66,10 +65,6 @@ private:
ErrorCode getXrayClients(const DockerContainer container, const ServerCredentials& credentials,
SshSession* sshSession, int &count, QJsonArray &clientsTable);
QString readXrayVolumeUuid(const DockerContainer container, const ServerCredentials &credentials,
SshSession *sshSession) const;
int stripXrayVolumeUuidFromTable(QJsonArray &clientsTable, const QString &volumeUuid);
ErrorCode wgShow(const DockerContainer container, const ServerCredentials &credentials,
SshSession* sshSession, std::vector<WgShowData> &data);

View File

@@ -8,7 +8,7 @@
#include "core/models/serverDescription.h"
#if defined(Q_OS_IOS) || defined(MACOS_NE)
#include "core/utils/swiftBridge.h"
#include <AmneziaVPN-Swift.h>
#endif
@@ -44,7 +44,6 @@ bool ServersController::renameServer(const QString &serverId, const QString &nam
auto cfg = m_serversRepository->selfHostedAdminConfig(serverId);
if (!cfg.has_value()) return false;
cfg->description = name;
cfg->displayName = name;
m_serversRepository->editServer(serverId, cfg->toJson(), kind);
return true;
}
@@ -52,7 +51,6 @@ bool ServersController::renameServer(const QString &serverId, const QString &nam
auto cfg = m_serversRepository->selfHostedUserConfig(serverId);
if (!cfg.has_value()) return false;
cfg->description = name;
cfg->displayName = name;
m_serversRepository->editServer(serverId, cfg->toJson(), kind);
return true;
}
@@ -60,7 +58,6 @@ bool ServersController::renameServer(const QString &serverId, const QString &nam
auto cfg = m_serversRepository->nativeConfig(serverId);
if (!cfg.has_value()) return false;
cfg->description = name;
cfg->displayName = name;
m_serversRepository->editServer(serverId, cfg->toJson(), kind);
return true;
}
@@ -70,7 +67,6 @@ bool ServersController::renameServer(const QString &serverId, const QString &nam
auto cfg = m_serversRepository->apiV2Config(serverId);
if (!cfg.has_value()) return false;
cfg->name = name;
cfg->displayName = name;
cfg->nameOverriddenByUser = true;
m_serversRepository->editServer(serverId, cfg->toJson(), kind);
return true;

View File

@@ -217,11 +217,6 @@ void SettingsController::toggleAutoStart(bool enable)
bool SettingsController::isStartMinimizedEnabled() const
{
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
if (!isAutoStartEnabled()) {
return false;
}
#endif
return m_appSettingsRepository->isStartMinimized();
}
@@ -351,8 +346,18 @@ void SettingsController::setAppLanguage(const QLocale &locale)
m_appSettingsRepository->setAppLanguage(locale);
}
QString SettingsController::nextAvailableServerName() const
bool SettingsController::isPremV1MigrationReminderActive() const
{
return m_serversRepository->nextAvailableServerName();
return m_appSettingsRepository->isPremV1MigrationReminderActive();
}
void SettingsController::disablePremV1MigrationReminder()
{
m_appSettingsRepository->disablePremV1MigrationReminder();
}
QString SettingsController::nextAvailableServerName() const
{
return m_appSettingsRepository->nextAvailableServerName();
}

View File

@@ -69,6 +69,9 @@ public:
void enableDevMode();
bool isPremV1MigrationReminderActive() const;
void disablePremV1MigrationReminder();
QString nextAvailableServerName() const;
bool isDevModeEnabled() const;

View File

@@ -5,14 +5,15 @@
#include <QUrl>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSysInfo>
#include <QTimer>
#include "amneziaApplication.h"
#include "logger.h"
#include "version.h"
#include "core/controllers/gatewayController.h"
#include "core/utils/api/gatewayPayloadBuilder.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/errorStrings.h"
#include "core/utils/selfhosted/scriptsRegistry.h"
namespace
@@ -20,13 +21,13 @@ namespace
Logger logger("UpdateController");
#if defined(Q_OS_WINDOWS)
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_windows_x64.exe");
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN-%1-win64.exe");
const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN_installer.exe";
#elif defined(Q_OS_MACOS) && !defined(MACOS_NE)
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_macos_x64.pkg");
#elif defined(Q_OS_MACOS)
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN-%1-Darwin.pkg");
const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.pkg";
#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN_%1_linux_x64.run");
const QLatin1String kInstallerRemoteFileNamePattern("AmneziaVPN-%1-Linux.run");
const QString kInstallerLocalPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.run";
#endif
}
@@ -95,20 +96,20 @@ void UpdateController::fetchGatewayUrl()
auto gatewayController = QSharedPointer<GatewayController>::create(m_appSettingsRepository->getGatewayEndpoint(),
m_appSettingsRepository->isDevGatewayEnv(),
7000,
m_appSettingsRepository->isStrictKillSwitchEnabled(),
m_appSettingsRepository);
m_appSettingsRepository->isStrictKillSwitchEnabled());
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
.addField(apiDefs::key::cliVersion, QString(APP_VERSION))
.build();
QJsonObject apiPayload;
apiPayload[apiDefs::key::cliVersion] = QString(APP_VERSION);
apiPayload[apiDefs::key::osVersion] = QSysInfo::productType();
apiPayload[apiDefs::key::installationUuid] = m_appSettingsRepository->getInstallationUuid(true);
// Workaround: wait before contacting gateway to avoid rate limit triggered by other requests (news etc.)
QTimer::singleShot(1000, this, [this, gatewayController, apiPayload]() {
gatewayController->postAsync(QStringLiteral("%1v1/updater_endpoint"), apiPayload)
.then(this, [this, gatewayController](QPair<ErrorCode, QByteArray> result) {
.then(this, [this](QPair<ErrorCode, QByteArray> result) {
auto [err, gatewayResponse] = result;
if (err != ErrorCode::NoError) {
logger.error() << "Gateway request failed, error code:" << static_cast<int>(err);
logger.error() << errorString(err);
finishUpdateCheck();
return;
}
@@ -183,7 +184,7 @@ void UpdateController::setupNetworkErrorHandling(QNetworkReply* reply, const QSt
logger.error() << QString("Network error occurred while fetching %1: %2 %3")
.arg(operation, reply->errorString(), QString::number(error));
});
QObject::connect(reply, &QNetworkReply::sslErrors, [operation](const QList<QSslError> &errors) {
QStringList errorStrings;
for (const QSslError &err : errors) {
@@ -195,13 +196,21 @@ void UpdateController::setupNetworkErrorHandling(QNetworkReply* reply, const QSt
void UpdateController::handleNetworkError(QNetworkReply* reply, const QString& operation)
{
logger.error() << "Network error code:" << QString::number(static_cast<int>(reply->error()));
logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError
|| reply->error() == QNetworkReply::NetworkError::TimeoutError) {
logger.error() << errorString(ErrorCode::ApiConfigTimeoutError);
} else {
QString err = reply->errorString();
logger.error() << "Network error code:" << QString::number(static_cast<int>(reply->error()));
logger.error() << "Error message:" << err;
logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
logger.error() << errorString(ErrorCode::ApiConfigDownloadError);
}
}
QString UpdateController::composeDownloadUrl() const
{
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
const QString fileName = QString(kInstallerRemoteFileNamePattern).arg(m_version);
return m_baseUrl + "/" + fileName;
#else
@@ -211,7 +220,7 @@ QString UpdateController::composeDownloadUrl() const
void UpdateController::runInstaller()
{
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
if (m_downloadUrl.isEmpty()) {
logger.error() << "Download URL is empty";
return;
@@ -243,15 +252,23 @@ void UpdateController::runInstaller()
#if defined(Q_OS_WINDOWS)
runWindowsInstaller(kInstallerLocalPath);
#elif defined(Q_OS_MACOS) && !defined(MACOS_NE)
#elif defined(Q_OS_MACOS)
runMacInstaller(kInstallerLocalPath);
#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
runLinuxInstaller(kInstallerLocalPath);
#endif
} else {
logger.error() << "Installer download failed, network error:" << static_cast<int>(reply->error())
<< reply->errorString();
logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError
|| reply->error() == QNetworkReply::NetworkError::TimeoutError) {
logger.error() << errorString(ErrorCode::ApiConfigTimeoutError);
} else {
QString err = reply->errorString();
logger.error() << QString::fromUtf8(reply->readAll());
logger.error() << "Network error code:" << QString::number(static_cast<int>(reply->error()));
logger.error() << "Error message:" << err;
logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
logger.error() << errorString(ErrorCode::ApiConfigDownloadError);
}
}
reply->deleteLater();
});
@@ -275,7 +292,7 @@ int UpdateController::runWindowsInstaller(const QString &installerPath)
}
#endif
#if defined(Q_OS_MACOS) && !defined(MACOS_NE)
#if defined(Q_OS_MACOS)
int UpdateController::runMacInstaller(const QString &installerPath)
{
// Create temporary directory for extraction

View File

@@ -1,16 +0,0 @@
#ifndef CONTAINERDIAGNOSTICS_H
#define CONTAINERDIAGNOSTICS_H
namespace amnezia
{
struct ContainerDiagnostics
{
bool available = false;
bool portReachable = false;
virtual ~ContainerDiagnostics() = default;
};
} // namespace amnezia
#endif // CONTAINERDIAGNOSTICS_H

View File

@@ -1,18 +0,0 @@
#ifndef MTPROXYDIAGNOSTICS_H
#define MTPROXYDIAGNOSTICS_H
#include "containerDiagnostics.h"
#include <QString>
namespace amnezia {
struct MtProxyDiagnostics : ContainerDiagnostics {
bool upstreamReachable = false;
int clientsConnected = -1;
QString lastConfigRefresh;
QString statsEndpoint;
};
} // namespace amnezia
#endif // MTPROXYDIAGNOSTICS_H

View File

@@ -1,20 +0,0 @@
#ifndef TELEMTDIAGNOSTICS_H
#define TELEMTDIAGNOSTICS_H
#include "containerDiagnostics.h"
#include <QString>
namespace amnezia
{
struct TelemtDiagnostics : ContainerDiagnostics
{
bool upstreamReachable = false;
int clientsConnected = -1;
QString lastConfigRefresh;
QString statsEndpoint;
};
} // namespace amnezia
#endif // TELEMTDIAGNOSTICS_H

View File

@@ -1,10 +1,11 @@
#include "awgInstaller.h"
#include <QPair>
#include <QRandomGenerator>
#include <QSet>
#include <QStringList>
#include <QVector>
#include "core/configurators/wireguardConfigurator.h"
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
@@ -27,59 +28,109 @@ AwgInstaller::AwgInstaller(QObject *parent)
ContainerConfig AwgInstaller::generateConfig(DockerContainer container, int port, TransportProto transportProto)
{
ContainerConfig config = createBaseConfig(container, port, transportProto);
bool isAwg2 = (container == DockerContainer::Awg2);
if (auto* awgConfig = config.getAwgProtocolConfig()) {
generateAwgParameters(awgConfig->serverConfig);
awgConfig->serverConfig.protocolVersion = protocols::awg::awgV3;
generateAwgParameters(awgConfig->serverConfig, isAwg2);
if (isAwg2) {
awgConfig->serverConfig.protocolVersion = "2";
}
}
return config;
}
void AwgInstaller::generateAwgParameters(AwgServerConfig &serverConfig)
void AwgInstaller::generateAwgParameters(AwgServerConfig &serverConfig, bool isAwg2)
{
QString junkPacketCount = QString::number(QRandomGenerator::global()->bounded(4, 7));
QString junkPacketMinSize = QString::number(10);
QString junkPacketMaxSize = QString::number(50);
int s1 = QRandomGenerator::global()->bounded(protocols::awg::junkPacketSizeMin, protocols::awg::initPacketJunkSizeMax);
int s2 = QRandomGenerator::global()->bounded(protocols::awg::junkPacketSizeMin, protocols::awg::responsePacketJunkSizeMax);
int s3 = QRandomGenerator::global()->bounded(protocols::awg::junkPacketSizeMin, protocols::awg::cookieReplyPacketJunkSizeMax);
int s4 = protocols::awg::defaultTransportPacketJunkSize;
int s1 = QRandomGenerator::global()->bounded(15, 150);
int s2 = QRandomGenerator::global()->bounded(15, 150);
int s3 = QRandomGenerator::global()->bounded(0, 64);
int s4 = QRandomGenerator::global()->bounded(0, 20);
// Ensure all values are unique and don't create equal packet sizes
QSet<int> usedValues { s1, s4 };
QSet<int> usedValues;
usedValues.insert(s1);
while (usedValues.contains(s2) || s1 + amnezia::AwgConstant::messageInitiationSize == s2 + amnezia::AwgConstant::messageResponseSize) {
s2 = QRandomGenerator::global()->bounded(protocols::awg::junkPacketSizeMin, protocols::awg::responsePacketJunkSizeMax);
s2 = QRandomGenerator::global()->bounded(15, 150);
}
usedValues.insert(s2);
while (usedValues.contains(s3) || s1 + amnezia::AwgConstant::messageInitiationSize == s3 + amnezia::AwgConstant::messageCookieReplySize
|| s2 + amnezia::AwgConstant::messageResponseSize == s3 + amnezia::AwgConstant::messageCookieReplySize) {
s3 = QRandomGenerator::global()->bounded(protocols::awg::junkPacketSizeMin, protocols::awg::cookieReplyPacketJunkSizeMax);
s3 = QRandomGenerator::global()->bounded(0, 64);
}
usedValues.insert(s3);
while (usedValues.contains(s4)) {
s4 = QRandomGenerator::global()->bounded(0, 20);
}
QString initPacketJunkSize = QString::number(s1);
QString responsePacketJunkSize = QString::number(s2);
QString cookieReplyPacketJunkSize = QString::number(s3);
QString transportPacketJunkSize = QString::number(s4);
QString initPacketMagicHeader;
QString responsePacketMagicHeader;
QString underloadPacketMagicHeader;
QString transportPacketMagicHeader;
if (isAwg2) {
// AWG 2.0: use range format for magic headers
QVector<QPair<QString, QString>> headersValue;
int min = 5;
auto max = (std::numeric_limits<qint32>::max)();
while (headersValue.size() != 4) {
auto first = QRandomGenerator::global()->bounded(min, max);
auto second = QRandomGenerator::global()->bounded(first, max);
min = second;
headersValue.push_back(QPair<QString, QString>(QString::number(first), QString::number(second)));
}
initPacketMagicHeader = headersValue.at(0).first + "-" + headersValue.at(0).second;
responsePacketMagicHeader = headersValue.at(1).first + "-" + headersValue.at(1).second;
underloadPacketMagicHeader = headersValue.at(2).first + "-" + headersValue.at(2).second;
transportPacketMagicHeader = headersValue.at(3).first + "-" + headersValue.at(3).second;
} else {
// AWG legacy: use single values for magic headers
QSet<QString> headersValue;
while (headersValue.size() != 4) {
auto max = (std::numeric_limits<qint32>::max)();
headersValue.insert(QString::number(QRandomGenerator::global()->bounded(5, max)));
}
auto headersValueList = headersValue.values();
initPacketMagicHeader = headersValueList.at(0);
responsePacketMagicHeader = headersValueList.at(1);
underloadPacketMagicHeader = headersValueList.at(2);
transportPacketMagicHeader = headersValueList.at(3);
}
serverConfig.junkPacketCount = junkPacketCount;
serverConfig.junkPacketMinSize = junkPacketMinSize;
serverConfig.junkPacketMaxSize = junkPacketMaxSize;
serverConfig.initPacketJunkSize = QString::number(s1);
serverConfig.responsePacketJunkSize = QString::number(s2);
serverConfig.cookieReplyPacketJunkSize = QString::number(s3);
serverConfig.transportPacketJunkSize = QString::number(s4);
serverConfig.initPacketJunkSize = initPacketJunkSize;
serverConfig.responsePacketJunkSize = responsePacketJunkSize;
serverConfig.initPacketMagicHeader = initPacketMagicHeader;
serverConfig.responsePacketMagicHeader = responsePacketMagicHeader;
serverConfig.underloadPacketMagicHeader = underloadPacketMagicHeader;
serverConfig.transportPacketMagicHeader = transportPacketMagicHeader;
serverConfig.initPacketMagicHeader = protocols::awg::defaultInitPacketMagicHeader;
serverConfig.responsePacketMagicHeader = protocols::awg::defaultResponsePacketMagicHeader;
serverConfig.underloadPacketMagicHeader = protocols::awg::defaultUnderloadPacketMagicHeader;
serverConfig.transportPacketMagicHeader = protocols::awg::defaultTransportPacketMagicHeader;
serverConfig.cookieReplyPacketJunkSize = cookieReplyPacketJunkSize;
serverConfig.transportPacketJunkSize = transportPacketJunkSize;
serverConfig.headerProtectionKey = WireguardConfigurator::genClientKeys().clientPrivKey;
serverConfig.contentPaddingAddition = protocols::awg::defaultContentPaddingAddition;
serverConfig.rekeyAfterTime = protocols::awg::defaultRekeyAfterTime;
serverConfig.rekeyTimeout = protocols::awg::defaultRekeyTimeout;
serverConfig.rejectAfterTime = protocols::awg::defaultRejectAfterTime;
serverConfig.keepaliveTimeout = protocols::awg::defaultKeepaliveTimeout;
serverConfig.maxHandshakeAttempts = protocols::awg::defaultMaxHandshakeAttempts;
serverConfig.randomTrailers = protocols::awg::defaultRandomTrailers;
serverConfig.disableCookies = protocols::awg::defaultDisableCookies;
serverConfig.specialJunk1 = protocols::awg::defaultSpecialJunk1;
serverConfig.specialJunk2 = protocols::awg::defaultSpecialJunk2;
serverConfig.specialJunk3 = protocols::awg::defaultSpecialJunk3;
serverConfig.specialJunk4 = protocols::awg::defaultSpecialJunk4;
serverConfig.specialJunk5 = protocols::awg::defaultSpecialJunk5;
}
ErrorCode AwgInstaller::extractConfigFromContainer(DockerContainer container, const ServerCredentials &credentials,
@@ -136,22 +187,14 @@ ErrorCode AwgInstaller::extractConfigFromContainer(DockerContainer container, co
awgConfig->serverConfig.specialJunk4 = serverConfigMap.value(QString("# ") + configKey::specialJunk4);
awgConfig->serverConfig.specialJunk5 = serverConfigMap.value(QString("# ") + configKey::specialJunk5);
awgConfig->serverConfig.cookieReplyPacketJunkSize = serverConfigMap.value(configKey::cookieReplyPacketJunkSize);
awgConfig->serverConfig.transportPacketJunkSize = serverConfigMap.value(configKey::transportPacketJunkSize);
awgConfig->serverConfig.headerProtectionKey = serverConfigMap.value(configKey::headerProtectionKey);
awgConfig->serverConfig.contentPaddingAddition = serverConfigMap.value(configKey::contentPaddingAddition);
awgConfig->serverConfig.rekeyAfterTime = serverConfigMap.value(configKey::rekeyAfterTime);
awgConfig->serverConfig.rekeyTimeout = serverConfigMap.value(configKey::rekeyTimeout);
awgConfig->serverConfig.rejectAfterTime = serverConfigMap.value(configKey::rejectAfterTime);
awgConfig->serverConfig.keepaliveTimeout = serverConfigMap.value(configKey::keepaliveTimeout);
awgConfig->serverConfig.maxHandshakeAttempts = serverConfigMap.value(configKey::maxHandshakeAttempts);
awgConfig->serverConfig.randomTrailers = serverConfigMap.value(configKey::randomTrailers);
awgConfig->serverConfig.disableCookies = serverConfigMap.value(configKey::disableCookies);
awgConfig->serverConfig.protocolVersion = awgConfig->serverProtocolVersion();
// AWG 2.0 specific fields
if (container == DockerContainer::Awg2) {
awgConfig->serverConfig.protocolVersion = "2";
awgConfig->serverConfig.cookieReplyPacketJunkSize = serverConfigMap.value(configKey::cookieReplyPacketJunkSize);
awgConfig->serverConfig.transportPacketJunkSize = serverConfigMap.value(configKey::transportPacketJunkSize);
}
}
return ErrorCode::NoError;
}

View File

@@ -14,7 +14,7 @@ public:
SshSession* serverController, amnezia::ContainerConfig &config) override;
private:
void generateAwgParameters(amnezia::AwgServerConfig &serverConfig);
void generateAwgParameters(amnezia::AwgServerConfig &serverConfig, bool isAwg2 = false);
};
#endif // AWGINSTALLER_H

View File

@@ -14,8 +14,6 @@
#include "core/models/protocols/xrayProtocolConfig.h"
#include "core/models/protocols/sftpProtocolConfig.h"
#include "core/models/protocols/socks5ProxyProtocolConfig.h"
#include "core/models/protocols/mtProxyProtocolConfig.h"
#include "core/models/protocols/telemtProtocolConfig.h"
#include "core/models/protocols/ikev2ProtocolConfig.h"
#include "core/models/protocols/torProtocolConfig.h"
@@ -56,7 +54,6 @@ ContainerConfig InstallerBase::createBaseConfig(DockerContainer container, int p
AwgProtocolConfig awgConfig;
awgConfig.serverConfig.port = portStr;
awgConfig.serverConfig.transportProto = transportProtoStr;
awgConfig.serverConfig.subnetAddress = protocols::wireguard::defaultSubnetAddress;
config.protocolConfig = awgConfig;
break;
}
@@ -64,7 +61,6 @@ ContainerConfig InstallerBase::createBaseConfig(DockerContainer container, int p
WireGuardProtocolConfig wgConfig;
wgConfig.serverConfig.port = portStr;
wgConfig.serverConfig.transportProto = transportProtoStr;
wgConfig.serverConfig.subnetAddress = protocols::wireguard::defaultSubnetAddress;
config.protocolConfig = wgConfig;
break;
}
@@ -78,18 +74,8 @@ ContainerConfig InstallerBase::createBaseConfig(DockerContainer container, int p
case Proto::Xray:
case Proto::SSXray: {
XrayProtocolConfig xrayConfig;
XrayServerConfig &srv = xrayConfig.serverConfig;
srv.port = portStr;
srv.transportProto = transportProtoStr;
srv.transport = protocols::xray::defaultTransport;
srv.security = protocols::xray::defaultSecurity;
srv.flow = protocols::xray::defaultFlow;
srv.site = protocols::xray::defaultSite;
srv.sni = protocols::xray::defaultSni;
srv.alpn = protocols::xray::defaultAlpn;
xrayConfig.clientTemplate.fingerprint = protocols::xray::defaultFingerprint;
xrayConfig.clientTemplate.uplinkMethod = protocols::xray::defaultXhttpUplinkMethod;
xrayConfig.clientTemplate.formatVersion = 1;
xrayConfig.serverConfig.port = portStr;
xrayConfig.serverConfig.transportProto = transportProtoStr;
config.protocolConfig = xrayConfig;
break;
}
@@ -105,18 +91,6 @@ ContainerConfig InstallerBase::createBaseConfig(DockerContainer container, int p
config.protocolConfig = socks5Config;
break;
}
case Proto::MtProxy: {
MtProxyProtocolConfig mtConfig;
mtConfig.port = portStr;
config.protocolConfig = mtConfig;
break;
}
case Proto::Telemt: {
TelemtProtocolConfig telemtConfig;
telemtConfig.port = portStr;
config.protocolConfig = telemtConfig;
break;
}
case Proto::Ikev2: {
Ikev2ProtocolConfig ikev2Config;
config.protocolConfig = ikev2Config;

View File

@@ -1,231 +0,0 @@
#include "mtProxyInstaller.h"
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/protocolEnum.h"
#include "core/utils/selfhosted/sshSession.h"
#include "core/models/containerConfig.h"
#include "core/models/protocols/mtProxyProtocolConfig.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QRegularExpression>
#include <QtGlobal>
using namespace amnezia;
namespace {
constexpr QLatin1String kMtProxyClientJsonPath("/data/amnezia-mtproxy-client.json");
constexpr QLatin1String kMtProxyClientJsonUploadPath("data/amnezia-mtproxy-client.json");
constexpr QLatin1String kMtProxySecretPath("/data/secret");
constexpr QLatin1String kMtProxyMetaPath("/data/mtproxy-meta");
constexpr QLatin1String kMtProxyStartScriptPath("/opt/amnezia/start.sh");
}
MtProxyInstaller::MtProxyInstaller(QObject *parent)
: InstallerBase(parent) {
}
ErrorCode MtProxyInstaller::extractConfigFromContainer(DockerContainer container, const ServerCredentials &credentials,
SshSession *sshSession, ContainerConfig &config) {
if (container != DockerContainer::MtProxy || !sshSession) {
return ErrorCode::NoError;
}
MtProxyProtocolConfig *mt = config.getMtProxyProtocolConfig();
if (!mt) {
return ErrorCode::NoError;
}
ErrorCode jsonErr = ErrorCode::NoError;
const QByteArray jsonRaw =
sshSession->getTextFileFromContainer(container, credentials, QString(kMtProxyClientJsonPath), jsonErr);
if (jsonErr == ErrorCode::NoError && !jsonRaw.trimmed().isEmpty()) {
QJsonParseError parseError;
const QJsonDocument doc = QJsonDocument::fromJson(jsonRaw.trimmed(), &parseError);
if (parseError.error == QJsonParseError::NoError && doc.isObject()) {
QJsonObject merged = mt->toJson();
const QJsonObject snap = doc.object();
for (auto it = snap.constBegin(); it != snap.constEnd(); ++it) {
merged.insert(it.key(), it.value());
}
*mt = MtProxyProtocolConfig::fromJson(merged);
}
}
static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$"));
const auto addExtra = [&](const QString &s) {
if (hex32.match(s).hasMatch() && !mt->additionalSecrets.contains(s)) {
mt->additionalSecrets.append(s);
}
};
ErrorCode secretErr = ErrorCode::NoError;
const QByteArray secretRaw =
sshSession->getTextFileFromContainer(container, credentials, QString(kMtProxySecretPath), secretErr);
const QString sec = QString::fromUtf8(secretRaw).trimmed();
if (sec.length() == 32 && hex32.match(sec).hasMatch()) {
mt->secret = sec;
}
bool metaRestored = false;
ErrorCode metaErr = ErrorCode::NoError;
const QByteArray metaRaw =
sshSession->getTextFileFromContainer(container, credentials, QString(kMtProxyMetaPath), metaErr);
if (metaErr == ErrorCode::NoError && !metaRaw.trimmed().isEmpty()) {
QString mode, domain, workersMode, workers, natInternal, natExternal;
bool natEnabled = false;
const QList<QByteArray> lines = metaRaw.split('\n');
for (const QByteArray &rawLine : lines) {
const QString line = QString::fromUtf8(rawLine).trimmed();
const int eq = line.indexOf('=');
if (eq < 0) {
continue;
}
const QString key = line.left(eq);
const QString val = line.mid(eq + 1).trimmed();
if (key == QLatin1String("mode")) mode = val;
else if (key == QLatin1String("domain")) domain = val;
else if (key == QLatin1String("tag")) { if (mt->tag.isEmpty()) mt->tag = val; }
else if (key == QLatin1String("additional")) {
for (const QString &s : val.split(',', Qt::SkipEmptyParts)) addExtra(s.trimmed());
}
else if (key == QLatin1String("workers_mode")) workersMode = val;
else if (key == QLatin1String("workers")) workers = val;
else if (key == QLatin1String("nat_enabled")) natEnabled = (val == QLatin1String("1"));
else if (key == QLatin1String("nat_internal")) natInternal = val;
else if (key == QLatin1String("nat_external")) natExternal = val;
else if (key == QLatin1String("public_host")) { if (mt->publicHost.isEmpty()) mt->publicHost = val; }
}
if (!mode.isEmpty()) {
mt->transportMode = mode;
if (!domain.isEmpty()) mt->tlsDomain = domain;
if (!workersMode.isEmpty()) mt->workersMode = workersMode;
if (workersMode == QLatin1String(protocols::mtProxy::workersModeManual) && !workers.isEmpty()) {
mt->workers = workers;
}
if (natEnabled) {
mt->natEnabled = true;
mt->natInternalIp = natInternal;
mt->natExternalIp = natExternal;
}
metaRestored = true;
}
}
if (!metaRestored) {
ErrorCode startErr = ErrorCode::NoError;
const QByteArray startRaw =
sshSession->getTextFileFromContainer(container, credentials, QString(kMtProxyStartScriptPath), startErr);
if (startErr == ErrorCode::NoError && !startRaw.trimmed().isEmpty()) {
const QString start = QString::fromUtf8(startRaw);
static const QRegularExpression modeRe(QStringLiteral("\\[ \"(standard|faketls)\" = \"faketls\" \\]"));
const QRegularExpressionMatch m = modeRe.match(start);
if (m.hasMatch()) {
mt->transportMode = m.captured(1);
if (m.captured(1) == QLatin1String(protocols::mtProxy::transportModeFakeTLS)) {
static const QRegularExpression domRe(QStringLiteral("--domain ([A-Za-z0-9.\\-]+)"));
const QRegularExpressionMatch dm = domRe.match(start);
if (dm.hasMatch()) mt->tlsDomain = dm.captured(1);
}
}
static const QRegularExpression tagRe(QStringLiteral("-P ([0-9a-fA-F]{32})"));
const QRegularExpressionMatch tm = tagRe.match(start);
if (tm.hasMatch() && mt->tag.isEmpty()) mt->tag = tm.captured(1);
static const QRegularExpression addRe(QStringLiteral("echo \"([0-9a-fA-F,]+)\" \\| tr ',' ' '"));
const QRegularExpressionMatch am = addRe.match(start);
if (am.hasMatch()) {
for (const QString &s : am.captured(1).split(',', Qt::SkipEmptyParts)) addExtra(s.trimmed());
}
static const QRegularExpression natRe(QStringLiteral("NAT_VALUE=\"([0-9.]+):([0-9.]+)\""));
const QRegularExpressionMatch nm = natRe.match(start);
if (nm.hasMatch()) {
mt->natEnabled = true;
mt->natInternalIp = nm.captured(1);
mt->natExternalIp = nm.captured(2);
}
}
}
return ErrorCode::NoError;
}
ErrorCode MtProxyInstaller::queryDiagnostics(SshSession &sshSession, const ServerCredentials &credentials,
DockerContainer container, int listenPort,
MtProxyContainerDiagnostics &out)
{
out = { };
if (container == DockerContainer::MtProxy || container == DockerContainer::Telemt) {
const QString containerName = ContainerUtils::containerToString(container);
const bool isTelemt = container == DockerContainer::Telemt;
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::InternalError;
}
void MtProxyInstaller::uploadClientSettingsSnapshot(SshSession &sshSession, const ServerCredentials &credentials,
DockerContainer container, const ContainerConfig &config) {
const MtProxyProtocolConfig *mt = config.getMtProxyProtocolConfig();
if (!mt) {
return;
}
const QByteArray payload = QJsonDocument(mt->toJson()).toJson(QJsonDocument::Compact);
const ErrorCode err = sshSession.uploadTextFileToContainer(container, credentials, QString::fromUtf8(payload),
QString(kMtProxyClientJsonUploadPath));
if (err != ErrorCode::NoError) {
qWarning() << "MtProxyInstaller::uploadClientSettingsSnapshot failed" << err;
}
}

View File

@@ -1,34 +0,0 @@
#ifndef MTPROXYINSTALLER_H
#define MTPROXYINSTALLER_H
#include "installerBase.h"
#include <QString>
struct MtProxyContainerDiagnostics {
bool portReachable = false;
bool upstreamReachable = false;
int clientsConnected = -1;
QString lastConfigRefresh;
QString statsEndpoint;
};
class MtProxyInstaller : public InstallerBase {
Q_OBJECT
public:
explicit MtProxyInstaller(QObject *parent = nullptr);
amnezia::ErrorCode
extractConfigFromContainer(amnezia::DockerContainer container, const amnezia::ServerCredentials &credentials,
SshSession *sshSession, amnezia::ContainerConfig &config) override;
static void uploadClientSettingsSnapshot(SshSession &sshSession, const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container,
const amnezia::ContainerConfig &config);
static amnezia::ErrorCode queryDiagnostics(SshSession &sshSession, const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container, int listenPort,
MtProxyContainerDiagnostics &out);
};
#endif // MTPROXYINSTALLER_H

View File

@@ -1,17 +1,15 @@
#include "socks5Installer.h"
#include "core/models/protocols/socks5ProxyProtocolConfig.h"
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
#include "core/utils/protocolEnum.h"
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/selfhosted/sshSession.h"
#include "core/utils/utilities.h"
#include <QRegularExpression>
using namespace amnezia;
using namespace ProtocolUtils;
@@ -35,29 +33,10 @@ ContainerConfig Socks5Installer::generateConfig(DockerContainer container, int p
ErrorCode Socks5Installer::extractConfigFromContainer(DockerContainer container, const ServerCredentials &credentials,
SshSession* sshSession, ContainerConfig &config)
{
if (container != DockerContainer::Socks5Proxy || !sshSession) {
return ErrorCode::NoError;
}
Socks5ProxyProtocolConfig *socks5Config = config.getSocks5ProxyProtocolConfig();
if (!socks5Config) {
return ErrorCode::NoError;
}
ErrorCode readError = ErrorCode::NoError;
const QByteArray configRaw = sshSession->getTextFileFromContainer(
container, credentials, QString::fromUtf8(protocols::socks5Proxy::proxyConfigPath), readError);
if (readError != ErrorCode::NoError || configRaw.trimmed().isEmpty()) {
return ErrorCode::NoError;
}
const QString proxyConfig = QString::fromUtf8(configRaw);
static const QRegularExpression usernameAndPasswordRegExp(QStringLiteral("users (\\w+):CL:(\\w+)"));
const QRegularExpressionMatch usernameAndPasswordMatch = usernameAndPasswordRegExp.match(proxyConfig);
if (usernameAndPasswordMatch.hasMatch()) {
socks5Config->userName = usernameAndPasswordMatch.captured(1);
socks5Config->password = usernameAndPasswordMatch.captured(2);
}
Q_UNUSED(container);
Q_UNUSED(credentials);
Q_UNUSED(sshSession);
Q_UNUSED(config);
return ErrorCode::NoError;
}

View File

@@ -1,154 +0,0 @@
#include "telemtInstaller.h"
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/selfhosted/sshSession.h"
#include "core/models/containerConfig.h"
#include "core/models/protocols/telemtProtocolConfig.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QRegularExpression>
#include <QtGlobal>
using namespace amnezia;
namespace {
constexpr QLatin1String kTelemtClientJsonPath("/data/amnezia-telemt-client.json");
constexpr QLatin1String kTelemtClientJsonUploadPath("data/amnezia-telemt-client.json");
constexpr QLatin1String kTelemtSecretPath("/data/secret");
constexpr QLatin1String kTelemtConfigTomlPath("/data/config.toml");
}
TelemtInstaller::TelemtInstaller(QObject *parent) : InstallerBase(parent) {}
ErrorCode TelemtInstaller::extractConfigFromContainer(DockerContainer container, const ServerCredentials &credentials,
SshSession *sshSession, ContainerConfig &config) {
if (container != DockerContainer::Telemt || !sshSession) {
return ErrorCode::NoError;
}
TelemtProtocolConfig *tc = config.getTelemtProtocolConfig();
if (!tc) {
return ErrorCode::NoError;
}
ErrorCode jsonErr = ErrorCode::NoError;
const QByteArray jsonRaw =
sshSession->getTextFileFromContainer(container, credentials, QString(kTelemtClientJsonPath), jsonErr);
if (jsonErr == ErrorCode::NoError && !jsonRaw.trimmed().isEmpty()) {
QJsonParseError parseError;
const QJsonDocument doc = QJsonDocument::fromJson(jsonRaw.trimmed(), &parseError);
if (parseError.error == QJsonParseError::NoError && doc.isObject()) {
QJsonObject merged = tc->toJson();
const QJsonObject snap = doc.object();
for (auto it = snap.constBegin(); it != snap.constEnd(); ++it) {
merged.insert(it.key(), it.value());
}
*tc = TelemtProtocolConfig::fromJson(merged);
}
}
ErrorCode secretErr = ErrorCode::NoError;
const QByteArray secretRaw =
sshSession->getTextFileFromContainer(container, credentials, QString(kTelemtSecretPath), secretErr);
const QString sec = QString::fromUtf8(secretRaw).trimmed();
static const QRegularExpression hex32(QStringLiteral("^[0-9a-fA-F]{32}$"));
if (sec.length() == 32 && hex32.match(sec).hasMatch()) {
tc->secret = sec;
}
ErrorCode tomlErr = ErrorCode::NoError;
const QByteArray tomlRaw =
sshSession->getTextFileFromContainer(container, credentials, QString(kTelemtConfigTomlPath), tomlErr);
if (tomlErr == ErrorCode::NoError && !tomlRaw.trimmed().isEmpty()) {
QString section;
bool userNameSet = false;
const QList<QByteArray> lines = tomlRaw.split('\n');
for (const QByteArray &rawLine : lines) {
const QString line = QString::fromUtf8(rawLine).trimmed();
if (line.isEmpty() || line.startsWith('#')) {
continue;
}
if (line.startsWith('[')) {
section = line;
continue;
}
const int eq = line.indexOf('=');
if (eq < 0) {
continue;
}
const QString key = line.left(eq).trimmed();
QString val = line.mid(eq + 1).trimmed();
if (val.startsWith('"')) {
const int last = val.lastIndexOf('"');
val = (last > 0) ? val.mid(1, last - 1) : val.mid(1);
} else {
const int inlineComment = val.indexOf(QLatin1String(" #"));
if (inlineComment >= 0) {
val = val.left(inlineComment).trimmed();
}
}
if (section == QLatin1String("[access.users]")) {
if (key.startsWith(QLatin1String("extra"))) {
if (hex32.match(val).hasMatch() && !tc->additionalSecrets.contains(val)) {
tc->additionalSecrets.append(val);
}
} else if (!userNameSet) {
tc->userName = key;
userNameSet = true;
if (tc->secret.isEmpty() && hex32.match(val).hasMatch()) {
tc->secret = val;
}
}
continue;
}
if (key == QLatin1String("tls") && section == QLatin1String("[general.modes]")) {
tc->transportMode = (val == QLatin1String("true"))
? QString::fromUtf8(protocols::telemt::transportModeFakeTLS)
: QString::fromUtf8(protocols::telemt::transportModeStandard);
} else if (key == QLatin1String("tls_domain") && section == QLatin1String("[censorship]")) {
tc->tlsDomain = val;
} else if (key == QLatin1String("mask") && section == QLatin1String("[censorship]")) {
tc->maskEnabled = (val == QLatin1String("true"));
} else if (key == QLatin1String("tls_emulation") && section == QLatin1String("[censorship]")) {
tc->tlsEmulation = (val == QLatin1String("true"));
} else if (key == QLatin1String("use_middle_proxy") && section == QLatin1String("[general]")) {
tc->useMiddleProxy = (val == QLatin1String("true"));
} else if (key == QLatin1String("middle_proxy_nat_ip") && section == QLatin1String("[general]")) {
if (!val.isEmpty()) {
tc->natExternalIp = val;
tc->natEnabled = true;
}
} else if (key == QLatin1String("ad_tag") && section == QLatin1String("[general]") && tc->tag.isEmpty()) {
tc->tag = val;
} else if (key == QLatin1String("public_host") && section == QLatin1String("[general.links]")
&& tc->publicHost.isEmpty()) {
tc->publicHost = val;
} else if (key == QLatin1String("port") && section == QLatin1String("[server]") && tc->port.isEmpty()) {
tc->port = val;
}
}
}
return ErrorCode::NoError;
}
void TelemtInstaller::uploadClientSettingsSnapshot(SshSession &sshSession, const ServerCredentials &credentials,
DockerContainer container, const ContainerConfig &config) {
const TelemtProtocolConfig *tc = config.getTelemtProtocolConfig();
if (!tc) {
return;
}
const QByteArray payload = QJsonDocument(tc->toJson()).toJson(QJsonDocument::Compact);
const ErrorCode err = sshSession.uploadTextFileToContainer(container, credentials, QString::fromUtf8(payload),
QString(kTelemtClientJsonUploadPath));
if (err != ErrorCode::NoError) {
qWarning() << "TelemtInstaller::uploadClientSettingsSnapshot failed" << err;
}
}

View File

@@ -1,20 +0,0 @@
#ifndef TELEMTINSTALLER_H
#define TELEMTINSTALLER_H
#include "installerBase.h"
class TelemtInstaller : public InstallerBase {
Q_OBJECT
public:
explicit TelemtInstaller(QObject *parent = nullptr);
amnezia::ErrorCode
extractConfigFromContainer(amnezia::DockerContainer container, const amnezia::ServerCredentials &credentials,
SshSession *sshSession, amnezia::ContainerConfig &config) override;
static void uploadClientSettingsSnapshot(SshSession &sshSession, const amnezia::ServerCredentials &credentials,
amnezia::DockerContainer container,
const amnezia::ContainerConfig &config);
};
#endif // TELEMTINSTALLER_H

View File

@@ -1,6 +1,7 @@
#include "xrayInstaller.h"
#include <QJsonDocument>
#include <QJsonArray>
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
@@ -10,30 +11,11 @@
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/selfhosted/sshSession.h"
#include "core/configurators/xrayConfigurator.h"
#include "core/models/protocols/xrayProtocolConfig.h"
#include "logger.h"
namespace
{
namespace {
Logger logger("XrayInstaller");
QString describeServerJsonStatus(amnezia::XrayServerJsonStatus status)
{
switch (status) {
case amnezia::XrayServerJsonStatus::MissingInbounds:
return QStringLiteral("server config missing 'inbounds' field");
case amnezia::XrayServerJsonStatus::EmptyInbounds:
return QStringLiteral("server config has empty 'inbounds' array");
case amnezia::XrayServerJsonStatus::MissingStreamSettings:
return QStringLiteral("inbound missing 'streamSettings' field");
case amnezia::XrayServerJsonStatus::MissingSettings:
return QStringLiteral("inbound missing 'settings' field");
case amnezia::XrayServerJsonStatus::Ok:
break;
}
return QStringLiteral("ok");
}
}
using namespace amnezia;
@@ -47,12 +29,10 @@ XrayInstaller::XrayInstaller(QObject *parent)
ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, const ServerCredentials &credentials,
SshSession* sshSession, ContainerConfig &config)
{
namespace px = amnezia::protocols::xray;
ErrorCode errorCode = ErrorCode::NoError;
QString currentConfig = sshSession->getTextFileFromContainer(
container, credentials, px::serverConfigPath, errorCode);
container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode);
if (errorCode != ErrorCode::NoError) {
return errorCode;
@@ -63,38 +43,38 @@ ErrorCode XrayInstaller::extractConfigFromContainer(DockerContainer container, c
logger.error() << "Failed to parse server config JSON";
return ErrorCode::InternalError;
}
auto *xrayConfig = config.getXrayProtocolConfig();
if (!xrayConfig) {
logger.error() << "No XrayProtocolConfig in ContainerConfig";
QJsonObject serverConfig = doc.object();
if (!serverConfig.contains(protocols::xray::inbounds)) {
logger.error() << "Server config missing 'inbounds' field";
return ErrorCode::InternalError;
}
XrayClientTemplate &tpl = xrayConfig->clientTemplate;
const amnezia::XrayServerJsonStatus status =
XrayServerConfig::fromServerInboundJson(doc.object(), xrayConfig->serverConfig, tpl);
if (status != amnezia::XrayServerJsonStatus::Ok) {
logger.error() << "Xray extractConfigFromContainer:" << describeServerJsonStatus(status);
QJsonArray inbounds = serverConfig[protocols::xray::inbounds].toArray();
if (inbounds.isEmpty()) {
logger.error() << "Server config has empty 'inbounds' array";
return ErrorCode::InternalError;
}
logger.info() << "Xray extractConfigFromContainer: extracted server, port=" << xrayConfig->serverConfig.port
<< "transport=" << xrayConfig->serverConfig.transport
<< "security=" << xrayConfig->serverConfig.security << "site=" << xrayConfig->serverConfig.site
<< "sni=" << xrayConfig->serverConfig.sni;
{
XrayConfigurator configurator(sshSession);
bool found = false;
const XrayClientTemplate stored = configurator.readClientTemplate(credentials, container, found);
if (found) {
tpl = stored;
logger.info() << "Xray extractConfigFromContainer: adopted the client template from the server,"
<< "fingerprint=" << tpl.fingerprint << "uplinkMethod=" << tpl.uplinkMethod;
} else {
logger.info() << "Xray extractConfigFromContainer: no client template on the server to adopt";
}
QJsonObject inbound = inbounds[0].toObject();
if (!inbound.contains(protocols::xray::streamSettings)) {
logger.error() << "Inbound missing 'streamSettings' field";
return ErrorCode::InternalError;
}
QJsonObject streamSettings = inbound[protocols::xray::streamSettings].toObject();
QJsonObject realitySettings = streamSettings[protocols::xray::realitySettings].toObject();
if (!realitySettings.contains(protocols::xray::serverNames)) {
logger.error() << "Settings missing 'serverNames' field";
return ErrorCode::InternalError;
}
QString siteName = realitySettings[protocols::xray::serverNames][0].toString();
if (auto* xrayConfig = config.getXrayProtocolConfig()) {
xrayConfig->serverConfig.site = siteName;
}
return ErrorCode::NoError;
}

View File

@@ -144,6 +144,10 @@ 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;
@@ -194,6 +198,7 @@ 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,6 +34,7 @@ struct ApiConfig
int maxDeviceCount;
int issuedConfigs;
QJsonArray availableCountries;
QJsonArray supportedProtocols;
struct ServiceInfo {
bool isAdVisible = false;

View File

@@ -13,7 +13,6 @@
#include "core/utils/api/apiUtils.h"
#include "core/models/api/apiConfig.h"
#include "core/models/api/authData.h"
#include "core/utils/networkUtilities.h"
namespace amnezia
{
@@ -68,20 +67,6 @@ ContainerConfig ApiV2ServerConfig::containerConfig(DockerContainer container) co
return containers.value(container);
}
QPair<QString, QString> ApiV2ServerConfig::getDnsPair(const QString &primaryDns, const QString &secondaryDns) const
{
QString d1 = dns1;
QString d2 = dns2;
if (d1.isEmpty() || !NetworkUtilities::checkIPv4Format(d1)) {
d1 = primaryDns;
}
if (d2.isEmpty() || !NetworkUtilities::checkIPv4Format(d2)) {
d2 = secondaryDns;
}
return { d1, d2 };
}
QJsonObject ApiV2ServerConfig::toJson() const
{
QJsonObject obj;
@@ -95,6 +80,9 @@ QJsonObject ApiV2ServerConfig::toJson() const
if (!description.isEmpty()) {
obj[configKey::description] = description;
}
if (!displayName.isEmpty()) {
obj[configKey::displayName] = displayName;
}
obj[configKey::configVersion] = configVersion;
@@ -121,11 +109,7 @@ 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;
}
@@ -150,6 +134,7 @@ ApiV2ServerConfig ApiV2ServerConfig::fromJson(const QJsonObject& json)
config.name = json.value(configKey::name).toString();
config.nameOverriddenByUser = json.value(configKey::nameOverriddenByUser).toBool(false);
config.description = json.value(configKey::description).toString();
config.displayName = json.value(configKey::displayName).toString();
config.configVersion = json.value(configKey::configVersion).toInt(2);
config.hostName = json.value(configKey::hostName).toString();
@@ -169,7 +154,6 @@ 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,10 +1,8 @@
#ifndef APIV2SERVERCONFIG_H
#define APIV2SERVERCONFIG_H
#include <QJsonArray>
#include <QJsonObject>
#include <QMap>
#include <QPair>
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
@@ -29,7 +27,6 @@ struct ApiV2ServerConfig {
DockerContainer defaultContainer;
QString dns1;
QString dns2;
QJsonArray sendPayload;
QString name;
bool nameOverriddenByUser = false;
@@ -46,9 +43,6 @@ struct ApiV2ServerConfig {
bool isExternalPremium() const;
bool hasContainers() const;
ContainerConfig containerConfig(DockerContainer container) const;
QPair<QString, QString> getDnsPair(const QString &primaryDns, const QString &secondaryDns) const;
QJsonObject toJson() const;
static ApiV2ServerConfig fromJson(const QJsonObject& json);
};

View File

@@ -23,7 +23,9 @@ LegacyApiServerConfig LegacyApiServerConfig::fromJson(const QJsonObject &json)
{
LegacyApiServerConfig config;
config.name = json.value(configKey::name).toString();
config.description = json.value(configKey::description).toString();
config.displayName = json.value(configKey::displayName).toString();
config.hostName = json.value(configKey::hostName).toString();
config.crc = json.value(configKey::crc).toInt(0);

View File

@@ -113,26 +113,6 @@ const Socks5ProxyProtocolConfig* ContainerConfig::getSocks5ProxyProtocolConfig()
return protocolConfig.as<Socks5ProxyProtocolConfig>();
}
MtProxyProtocolConfig* ContainerConfig::getMtProxyProtocolConfig()
{
return protocolConfig.as<MtProxyProtocolConfig>();
}
const MtProxyProtocolConfig* ContainerConfig::getMtProxyProtocolConfig() const
{
return protocolConfig.as<MtProxyProtocolConfig>();
}
TelemtProtocolConfig* ContainerConfig::getTelemtProtocolConfig()
{
return protocolConfig.as<TelemtProtocolConfig>();
}
const TelemtProtocolConfig* ContainerConfig::getTelemtProtocolConfig() const
{
return protocolConfig.as<TelemtProtocolConfig>();
}
Ikev2ProtocolConfig* ContainerConfig::getIkev2ProtocolConfig()
{
return protocolConfig.as<Ikev2ProtocolConfig>();

View File

@@ -57,12 +57,6 @@ struct ContainerConfig {
Socks5ProxyProtocolConfig* getSocks5ProxyProtocolConfig();
const Socks5ProxyProtocolConfig* getSocks5ProxyProtocolConfig() const;
MtProxyProtocolConfig* getMtProxyProtocolConfig();
const MtProxyProtocolConfig* getMtProxyProtocolConfig() const;
TelemtProtocolConfig* getTelemtProtocolConfig();
const TelemtProtocolConfig* getTelemtProtocolConfig() const;
Ikev2ProtocolConfig* getIkev2ProtocolConfig();
const Ikev2ProtocolConfig* getIkev2ProtocolConfig() const;

View File

@@ -9,8 +9,6 @@
#include "core/utils/protocolEnum.h"
#include "core/models/protocols/ikev2ProtocolConfig.h"
#include "core/models/protocols/dnsProtocolConfig.h"
#include "core/models/protocols/mtProxyProtocolConfig.h"
#include "core/models/protocols/telemtProtocolConfig.h"
namespace amnezia
{
@@ -40,10 +38,6 @@ Proto ProtocolConfig::type() const
return Proto::TorWebSite;
} else if constexpr (std::is_same_v<T, DnsProtocolConfig>) {
return Proto::Dns;
} else if constexpr (std::is_same_v<T, MtProxyProtocolConfig>) {
return Proto::MtProxy;
} else if constexpr (std::is_same_v<T, TelemtProtocolConfig>) {
return Proto::Telemt;
}
return Proto::Unknown;
}, data);
@@ -71,10 +65,6 @@ QString ProtocolConfig::port() const
return QString();
} else if constexpr (std::is_same_v<T, DnsProtocolConfig>) {
return QString();
} else if constexpr (std::is_same_v<T, MtProxyProtocolConfig>) {
return arg.port.isEmpty() ? QString(protocols::mtProxy::defaultPort) : arg.port;
} else if constexpr (std::is_same_v<T, TelemtProtocolConfig>) {
return arg.port.isEmpty() ? QString(protocols::telemt::defaultPort) : arg.port;
}
return QString();
}, data);
@@ -98,10 +88,6 @@ QString ProtocolConfig::transportProto() const
return QString();
} else if constexpr (std::is_same_v<T, DnsProtocolConfig>) {
return QString();
} else if constexpr (std::is_same_v<T, MtProxyProtocolConfig>) {
return QStringLiteral("tcp");
} else if constexpr (std::is_same_v<T, TelemtProtocolConfig>) {
return QStringLiteral("tcp");
}
return QString();
}, data);
@@ -313,10 +299,6 @@ ProtocolConfig ProtocolConfig::fromJson(const QJsonObject& json, Proto type)
return ProtocolConfig{TorProtocolConfig::fromJson(json)};
case Proto::Dns:
return ProtocolConfig{DnsProtocolConfig::fromJson(json)};
case Proto::MtProxy:
return ProtocolConfig{MtProxyProtocolConfig::fromJson(json)};
case Proto::Telemt:
return ProtocolConfig{TelemtProtocolConfig::fromJson(json)};
default:
return ProtocolConfig{AwgProtocolConfig{}};
}

View File

@@ -22,8 +22,6 @@
#include "core/models/protocols/ikev2ProtocolConfig.h"
#include "core/models/protocols/torProtocolConfig.h"
#include "core/models/protocols/dnsProtocolConfig.h"
#include "core/models/protocols/mtProxyProtocolConfig.h"
#include "core/models/protocols/telemtProtocolConfig.h"
namespace amnezia
{
@@ -38,8 +36,6 @@ struct ProtocolConfig {
XrayProtocolConfig,
SftpProtocolConfig,
Socks5ProxyProtocolConfig,
MtProxyProtocolConfig,
TelemtProtocolConfig,
Ikev2ProtocolConfig,
TorProtocolConfig,
DnsProtocolConfig

View File

@@ -2,10 +2,6 @@
#include <QJsonDocument>
#include <QJsonArray>
#include <QObject>
#include <QSet>
#include <algorithm>
#include "../../../core/utils/protocolEnum.h"
#include "../../../core/protocols/protocolUtils.h"
@@ -17,61 +13,6 @@ using namespace ProtocolUtils;
namespace amnezia
{
namespace
{
// A toggle set to "off" behaves exactly like a missing one, so it is not an AWG 3 marker
bool isAwgToggleEnabled(const QString &value)
{
const QString trimmedValue = value.trimmed();
return !trimmedValue.isEmpty()
&& trimmedValue.compare(QLatin1String(protocols::awg::awgBoolOff), Qt::CaseInsensitive) != 0;
}
template <typename T>
bool hasAwg3Markers(const T &config)
{
auto hasValue = [](const QString &value) { return !value.trimmed().isEmpty(); };
const QStringList awg3Params = { config.headerProtectionKey, config.contentPaddingAddition,
config.rekeyAfterTime, config.rekeyTimeout,
config.rejectAfterTime, config.keepaliveTimeout,
config.maxHandshakeAttempts };
if (std::any_of(awg3Params.begin(), awg3Params.end(), hasValue)) {
return true;
}
return isAwgToggleEnabled(config.randomTrailers) || isAwgToggleEnabled(config.disableCookies);
}
template <typename T>
QString awgVersionOf(const T &config)
{
auto hasValue = [](const QString &value) { return !value.trimmed().isEmpty(); };
if (hasAwg3Markers(config)) {
return protocols::awg::awgV3;
}
const QStringList junkSizes = { config.cookieReplyPacketJunkSize, config.transportPacketJunkSize };
const QStringList magicHeaders = { config.initPacketMagicHeader, config.responsePacketMagicHeader,
config.underloadPacketMagicHeader, config.transportPacketMagicHeader };
bool hasJunkSizes = std::any_of(junkSizes.begin(), junkSizes.end(), hasValue);
bool hasHeaderRanges = std::any_of(magicHeaders.begin(), magicHeaders.end(),
[](const QString &header) { return header.contains('-'); });
if (hasJunkSizes || hasHeaderRanges) {
return protocols::awg::awgV2;
}
const QStringList specialJunk = { config.specialJunk1, config.specialJunk2, config.specialJunk3,
config.specialJunk4, config.specialJunk5 };
if (std::any_of(specialJunk.begin(), specialJunk.end(), hasValue)) {
return protocols::awg::awgV1_5;
}
return QString();
}
} // namespace
QJsonObject AwgServerConfig::toJson() const
{
QJsonObject obj;
@@ -133,34 +74,6 @@ 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 (!randomTrailers.isEmpty()) {
obj[configKey::randomTrailers] = randomTrailers;
}
if (!disableCookies.isEmpty()) {
obj[configKey::disableCookies] = disableCookies;
}
if (isThirdPartyConfig) {
obj[configKey::isThirdPartyConfig] = isThirdPartyConfig;
}
@@ -196,17 +109,7 @@ 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.randomTrailers = json.value(configKey::randomTrailers).toString();
config.disableCookies = json.value(configKey::disableCookies).toString();
config.isThirdPartyConfig = json.value(configKey::isThirdPartyConfig).toBool(false);
return config;
@@ -293,50 +196,16 @@ QJsonObject AwgClientConfig::toJson() const
obj[configKey::transportPacketMagicHeader] = transportPacketMagicHeader;
}
if (!specialJunk1.isEmpty()) {
obj[configKey::specialJunk1] = specialJunk1;
obj[configKey::specialJunk1] = specialJunk1;
obj[configKey::specialJunk2] = specialJunk2;
obj[configKey::specialJunk3] = specialJunk3;
obj[configKey::specialJunk4] = specialJunk4;
obj[configKey::specialJunk5] = specialJunk5;
if (isObfuscationEnabled) {
obj[configKey::isObfuscationEnabled] = isObfuscationEnabled;
}
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 (!randomTrailers.isEmpty()) {
obj[configKey::randomTrailers] = randomTrailers;
}
if (!disableCookies.isEmpty()) {
obj[configKey::disableCookies] = disableCookies;
}
return obj;
}
@@ -379,28 +248,21 @@ 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.randomTrailers = json.value(configKey::randomTrailers).toString();
config.disableCookies = json.value(configKey::disableCookies).toString();
config.isObfuscationEnabled = json.value(configKey::isObfuscationEnabled).toBool(false);
return config;
}
QJsonObject AwgProtocolConfig::toJson() const
{
QJsonObject obj = serverConfig.toJson();
if (clientConfig.has_value()) {
QJsonObject clientJson = clientConfig->toJson();
obj[configKey::lastConfig] = QString::fromUtf8(QJsonDocument(clientJson).toJson(QJsonDocument::Compact));
}
return obj;
}
@@ -421,24 +283,6 @@ AwgProtocolConfig AwgProtocolConfig::fromJson(const QJsonObject& json)
return config;
}
QString AwgProtocolConfig::serverProtocolVersion() const
{
return awgVersionOf(serverConfig);
}
QString AwgProtocolConfig::clientProtocolVersion() const
{
return clientConfig.has_value() ? awgVersionOf(clientConfig.value()) : QString();
}
QString AwgProtocolConfig::protocolVersionString(const QString &version)
{
if (version == protocols::awg::awgV3) return QObject::tr(" (version 3.1)");
if (version == protocols::awg::awgV2) return QObject::tr(" (version 2)");
if (version == protocols::awg::awgV1_5) return QObject::tr(" (version 1.5)");
return "";
}
bool AwgProtocolConfig::hasClientConfig() const
{
return clientConfig.has_value();
@@ -466,37 +310,24 @@ 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 ||
cookieReplyPacketJunkSize != other.cookieReplyPacketJunkSize ||
transportPacketJunkSize != other.transportPacketJunkSize ||
headerProtectionKey != other.headerProtectionKey ||
contentPaddingAddition != other.contentPaddingAddition ||
rekeyAfterTime != other.rekeyAfterTime || rekeyTimeout != other.rekeyTimeout ||
rejectAfterTime != other.rejectAfterTime || keepaliveTimeout != other.keepaliveTimeout ||
maxHandshakeAttempts != other.maxHandshakeAttempts ||
randomTrailers != other.randomTrailers || disableCookies != other.disableCookies) {
specialJunk5 != other.specialJunk5) {
return false;
}
bool isV2 = protocolVersion == protocols::awg::awgV2;
if (isV2) {
if (cookieReplyPacketJunkSize != other.cookieReplyPacketJunkSize ||
transportPacketJunkSize != other.transportPacketJunkSize) {
return false;
}
}
return true;
}
bool AwgServerConfig::hasAwg3Params() const
{
return hasAwg3Markers(*this);
}
bool AwgProtocolConfig::isHeadersEqual(const QString &h1, const QString &h2, const QString &h3, const QString &h4)
{
QSet<QString> uniqueHeaders;
int filledHeaders = 0;
for (const QString &header : { h1, h2, h3, h4 }) {
if (!header.trimmed().isEmpty()) {
++filledHeaders;
uniqueHeaders.insert(header);
}
}
return uniqueHeaders.size() != filledHeaders;
return (h1 == h2) || (h1 == h3) || (h1 == h4) || (h2 == h3) || (h2 == h4) || (h3 == h4);
}
bool AwgProtocolConfig::isPacketSizeEqual(int s1, int s2, int s3, int s4)

View File

@@ -39,22 +39,12 @@ struct AwgServerConfig {
QString specialJunk3;
QString specialJunk4;
QString specialJunk5;
QString headerProtectionKey;
QString contentPaddingAddition;
QString rekeyAfterTime;
QString rekeyTimeout;
QString rejectAfterTime;
QString keepaliveTimeout;
QString maxHandshakeAttempts;
QString randomTrailers;
QString disableCookies;
bool isThirdPartyConfig = false;
QJsonObject toJson() const;
static AwgServerConfig fromJson(const QJsonObject& json);
bool hasEqualServerSettings(const AwgServerConfig& other) const;
bool hasAwg3Params() const;
};
struct AwgClientConfig {
@@ -86,15 +76,8 @@ struct AwgClientConfig {
QString specialJunk3;
QString specialJunk4;
QString specialJunk5;
QString headerProtectionKey;
QString contentPaddingAddition;
QString rekeyAfterTime;
QString rekeyTimeout;
QString rejectAfterTime;
QString keepaliveTimeout;
QString maxHandshakeAttempts;
QString randomTrailers;
QString disableCookies;
bool isObfuscationEnabled = false;
QJsonObject toJson() const;
static AwgClientConfig fromJson(const QJsonObject& json);
};
@@ -106,10 +89,6 @@ struct AwgProtocolConfig {
QJsonObject toJson() const;
static AwgProtocolConfig fromJson(const QJsonObject& json);
QString serverProtocolVersion() const;
QString clientProtocolVersion() const;
static QString protocolVersionString(const QString &version);
bool hasClientConfig() const;
void setClientConfig(const AwgClientConfig& config);
void clearClientConfig();

View File

@@ -1,147 +0,0 @@
#include "mtProxyProtocolConfig.h"
#include "../../../core/utils/protocolEnum.h"
#include "../../../core/protocols/protocolUtils.h"
#include "../../../core/utils/constants/configKeys.h"
#include "../../../core/utils/constants/protocolConstants.h"
#include <QJsonArray>
#include <algorithm>
using namespace amnezia;
namespace amnezia {
QJsonObject MtProxyProtocolConfig::toJson() const {
QJsonObject obj;
if (!port.isEmpty()) {
obj[configKey::port] = port;
}
if (!secret.isEmpty()) {
obj[protocols::mtProxy::secretKey] = secret;
}
if (!tag.isEmpty()) {
obj[protocols::mtProxy::tagKey] = tag;
}
if (!tgLink.isEmpty()) {
obj[protocols::mtProxy::tgLinkKey] = tgLink;
}
if (!tmeLink.isEmpty()) {
obj[protocols::mtProxy::tmeLinkKey] = tmeLink;
}
obj[protocols::mtProxy::isEnabledKey] = isEnabled;
if (!publicHost.isEmpty()) {
obj[protocols::mtProxy::publicHostKey] = publicHost;
}
if (!transportMode.isEmpty()) {
obj[protocols::mtProxy::transportModeKey] = transportMode;
}
if (!tlsDomain.isEmpty()) {
obj[protocols::mtProxy::tlsDomainKey] = tlsDomain;
}
if (!additionalSecrets.isEmpty()) {
obj[protocols::mtProxy::additionalSecretsKey] = QJsonArray::fromStringList(additionalSecrets);
}
if (!workersMode.isEmpty()) {
obj[protocols::mtProxy::workersModeKey] = workersMode;
}
if (!workers.isEmpty()) {
obj[protocols::mtProxy::workersKey] = workers;
}
obj[protocols::mtProxy::natEnabledKey] = natEnabled;
if (!natInternalIp.isEmpty()) {
obj[protocols::mtProxy::natInternalIpKey] = natInternalIp;
}
if (!natExternalIp.isEmpty()) {
obj[protocols::mtProxy::natExternalIpKey] = natExternalIp;
}
return obj;
}
MtProxyProtocolConfig MtProxyProtocolConfig::fromJson(const QJsonObject &json) {
MtProxyProtocolConfig config;
config.port = json.value(configKey::port).toString();
config.secret = json.value(protocols::mtProxy::secretKey).toString();
config.tag = json.value(protocols::mtProxy::tagKey).toString();
config.tgLink = json.value(protocols::mtProxy::tgLinkKey).toString();
config.tmeLink = json.value(protocols::mtProxy::tmeLinkKey).toString();
config.isEnabled = json.value(protocols::mtProxy::isEnabledKey).toBool(true);
config.publicHost = json.value(protocols::mtProxy::publicHostKey).toString();
config.transportMode = json.value(protocols::mtProxy::transportModeKey).toString();
config.tlsDomain = json.value(protocols::mtProxy::tlsDomainKey).toString();
for (const auto &v: json.value(protocols::mtProxy::additionalSecretsKey).toArray()) {
const QString s = v.toString();
if (!s.isEmpty()) {
config.additionalSecrets.append(s);
}
}
config.workersMode = json.value(protocols::mtProxy::workersModeKey).toString();
config.workers = json.value(protocols::mtProxy::workersKey).toString();
config.natEnabled = json.value(protocols::mtProxy::natEnabledKey).toBool(false);
config.natInternalIp = json.value(protocols::mtProxy::natInternalIpKey).toString();
config.natExternalIp = json.value(protocols::mtProxy::natExternalIpKey).toString();
return config;
}
bool MtProxyProtocolConfig::equalsDockerDeploymentSettings(const MtProxyProtocolConfig &other) const {
const auto normPort = [](const QString &p) {
return p.isEmpty() ? QString(protocols::mtProxy::defaultPort) : p;
};
const auto normTransport = [](const QString &t) {
return t.isEmpty() ? QString(protocols::mtProxy::transportModeStandard) : t;
};
const auto normWorkersMode = [](const QString &m) {
return m.isEmpty() ? QString(protocols::mtProxy::workersModeAuto) : m;
};
if (normPort(port) != normPort(other.port)) {
return false;
}
if (normTransport(transportMode) != normTransport(other.transportMode)) {
return false;
}
if (tlsDomain != other.tlsDomain) {
return false;
}
if (secret != other.secret) {
return false;
}
if (tag != other.tag) {
return false;
}
if (publicHost != other.publicHost) {
return false;
}
if (normWorkersMode(workersMode) != normWorkersMode(other.workersMode)) {
return false;
}
if (workers != other.workers) {
return false;
}
if (natEnabled != other.natEnabled) {
return false;
}
if (natInternalIp != other.natInternalIp) {
return false;
}
if (natExternalIp != other.natExternalIp) {
return false;
}
if (isEnabled != other.isEnabled) {
return false;
}
QStringList aa = additionalSecrets;
QStringList bb = other.additionalSecrets;
aa.removeAll(QString());
bb.removeAll(QString());
std::sort(aa.begin(), aa.end());
std::sort(bb.begin(), bb.end());
return aa == bb;
}
} // namespace amnezia

View File

@@ -1,38 +0,0 @@
#ifndef MTPROXYPROTOCOLCONFIG_H
#define MTPROXYPROTOCOLCONFIG_H
#include <QJsonObject>
#include <QString>
#include <QStringList>
namespace amnezia {
struct MtProxyProtocolConfig {
QString port;
QString secret;
QString tag;
QString tgLink;
QString tmeLink;
bool isEnabled = true;
QString publicHost;
QString transportMode;
QString tlsDomain;
QStringList additionalSecrets;
QString workersMode;
QString workers;
bool natEnabled = false;
QString natInternalIp;
QString natExternalIp;
QJsonObject toJson() const;
static MtProxyProtocolConfig fromJson(const QJsonObject &json);
// Port, transport, TLS, secrets, NAT, workers, isEnabled, additionalSecrets (order-independent).
// Ignores tgLink / tmeLink (derived / display).
bool equalsDockerDeploymentSettings(const MtProxyProtocolConfig &other) const;
};
} // namespace amnezia
#endif // MTPROXYPROTOCOLCONFIG_H

View File

@@ -1,150 +0,0 @@
#include "telemtProtocolConfig.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
#include <QJsonArray>
#include <algorithm>
using namespace amnezia;
QJsonObject TelemtProtocolConfig::toJson() const
{
QJsonObject obj;
if (!port.isEmpty()) {
obj[QString(configKey::port)] = port;
}
if (!secret.isEmpty()) {
obj[protocols::telemt::secretKey] = secret;
}
if (!tag.isEmpty()) {
obj[protocols::telemt::tagKey] = tag;
}
if (!tgLink.isEmpty()) {
obj[protocols::telemt::tgLinkKey] = tgLink;
}
if (!tmeLink.isEmpty()) {
obj[protocols::telemt::tmeLinkKey] = tmeLink;
}
obj[protocols::telemt::isEnabledKey] = isEnabled;
if (!publicHost.isEmpty()) {
obj[protocols::telemt::publicHostKey] = publicHost;
}
if (!transportMode.isEmpty()) {
obj[protocols::telemt::transportModeKey] = transportMode;
}
if (!tlsDomain.isEmpty()) {
obj[protocols::telemt::tlsDomainKey] = tlsDomain;
}
obj[protocols::telemt::maskEnabledKey] = maskEnabled;
obj[protocols::telemt::tlsEmulationKey] = tlsEmulation;
obj[protocols::telemt::useMiddleProxyKey] = useMiddleProxy;
if (!userName.isEmpty()) {
obj[protocols::telemt::userNameKey] = userName;
}
if (!additionalSecrets.isEmpty()) {
obj[protocols::telemt::additionalSecretsKey] = QJsonArray::fromStringList(additionalSecrets);
}
if (!workersMode.isEmpty()) {
obj[protocols::telemt::workersModeKey] = workersMode;
}
if (!workers.isEmpty()) {
obj[protocols::telemt::workersKey] = workers;
}
obj[protocols::telemt::natEnabledKey] = natEnabled;
if (!natInternalIp.isEmpty()) {
obj[protocols::telemt::natInternalIpKey] = natInternalIp;
}
if (!natExternalIp.isEmpty()) {
obj[protocols::telemt::natExternalIpKey] = natExternalIp;
}
return obj;
}
TelemtProtocolConfig TelemtProtocolConfig::fromJson(const QJsonObject &json)
{
TelemtProtocolConfig c;
c.port = json.value(QString(configKey::port)).toString();
c.secret = json.value(protocols::telemt::secretKey).toString();
c.tag = json.value(protocols::telemt::tagKey).toString();
c.tgLink = json.value(protocols::telemt::tgLinkKey).toString();
c.tmeLink = json.value(protocols::telemt::tmeLinkKey).toString();
c.isEnabled = json.value(protocols::telemt::isEnabledKey).toBool(true);
c.publicHost = json.value(protocols::telemt::publicHostKey).toString();
c.transportMode = json.value(protocols::telemt::transportModeKey).toString();
c.tlsDomain = json.value(protocols::telemt::tlsDomainKey).toString();
c.maskEnabled = json.value(protocols::telemt::maskEnabledKey).toBool(true);
c.tlsEmulation = json.value(protocols::telemt::tlsEmulationKey).toBool(false);
c.useMiddleProxy = json.value(protocols::telemt::useMiddleProxyKey).toBool(true);
c.userName = json.value(protocols::telemt::userNameKey).toString();
for (const auto &v : json.value(protocols::telemt::additionalSecretsKey).toArray()) {
const QString s = v.toString();
if (!s.isEmpty()) {
c.additionalSecrets.append(s);
}
}
c.workersMode = json.value(protocols::telemt::workersModeKey).toString();
c.workers = json.value(protocols::telemt::workersKey).toString();
c.natEnabled = json.value(protocols::telemt::natEnabledKey).toBool(false);
c.natInternalIp = json.value(protocols::telemt::natInternalIpKey).toString();
c.natExternalIp = json.value(protocols::telemt::natExternalIpKey).toString();
return c;
}
bool TelemtProtocolConfig::equalsDockerDeploymentSettings(const TelemtProtocolConfig &other) const
{
const auto normPort = [](const QString &p) {
return p.isEmpty() ? QString(protocols::telemt::defaultPort) : p;
};
const auto normTransport = [](const QString &t) {
return t.isEmpty() ? QString(protocols::telemt::transportModeStandard) : t;
};
if (normPort(port) != normPort(other.port)) {
return false;
}
if (normTransport(transportMode) != normTransport(other.transportMode)) {
return false;
}
if (tlsDomain != other.tlsDomain) {
return false;
}
if (secret != other.secret) {
return false;
}
if (tag != other.tag) {
return false;
}
if (publicHost != other.publicHost) {
return false;
}
if (maskEnabled != other.maskEnabled) {
return false;
}
if (tlsEmulation != other.tlsEmulation) {
return false;
}
if (useMiddleProxy != other.useMiddleProxy) {
return false;
}
if (userName != other.userName) {
return false;
}
if (natEnabled != other.natEnabled) {
return false;
}
if (natExternalIp != other.natExternalIp) {
return false;
}
if (isEnabled != other.isEnabled) {
return false;
}
QStringList aa = additionalSecrets;
QStringList bb = other.additionalSecrets;
aa.removeAll(QString());
bb.removeAll(QString());
std::sort(aa.begin(), aa.end());
std::sort(bb.begin(), bb.end());
return aa == bb;
}

View File

@@ -1,38 +0,0 @@
#ifndef TELEMTPROTOCOLCONFIG_H
#define TELEMTPROTOCOLCONFIG_H
#include <QJsonObject>
#include <QString>
#include <QStringList>
namespace amnezia {
struct TelemtProtocolConfig {
QString port;
QString secret;
QString tag;
QString tgLink;
QString tmeLink;
bool isEnabled = true;
QString publicHost;
QString transportMode;
QString tlsDomain;
bool maskEnabled = true;
bool tlsEmulation = false;
bool useMiddleProxy = true;
QString userName;
QStringList additionalSecrets;
QString workersMode;
QString workers;
bool natEnabled = false;
QString natInternalIp;
QString natExternalIp;
QJsonObject toJson() const;
static TelemtProtocolConfig fromJson(const QJsonObject &json);
bool equalsDockerDeploymentSettings(const TelemtProtocolConfig &other) const;
};
} // namespace amnezia
#endif // TELEMTPROTOCOLCONFIG_H

View File

@@ -105,17 +105,11 @@ QJsonObject WireGuardClientConfig::toJson() const
if (!mtu.isEmpty()) {
obj[configKey::mtu] = mtu;
}
for (auto it = awgParams.constBegin(); it != awgParams.constEnd(); ++it) {
if (!it.value().isEmpty()) {
obj[it.key()] = it.value();
}
}
if (isObfuscationEnabled) {
obj[configKey::isObfuscationEnabled] = isObfuscationEnabled;
}
return obj;
}
@@ -139,16 +133,9 @@ WireGuardClientConfig WireGuardClientConfig::fromJson(const QJsonObject& json)
}
config.persistentKeepAlive = json.value(configKey::persistentKeepAlive).toString();
config.mtu = json.value(configKey::mtu).toString();
for (const QString &key : configKey::awgProtocolKeys()) {
const QString value = json.value(key).toString();
if (!value.isEmpty()) {
config.awgParams.insert(key, value);
}
}
config.isObfuscationEnabled = json.value(configKey::isObfuscationEnabled).toBool(false);
return config;
}

View File

@@ -2,7 +2,6 @@
#define WIREGUARDPROTOCOLCONFIG_H
#include <QJsonObject>
#include <QMap>
#include <QString>
#include <QStringList>
#include <optional>
@@ -37,10 +36,8 @@ struct WireGuardClientConfig {
QStringList allowedIps;
QString persistentKeepAlive;
QString mtu;
QMap<QString, QString> awgParams;
bool isObfuscationEnabled = false;
QJsonObject toJson() const;
static WireGuardClientConfig fromJson(const QJsonObject& json);
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,284 +1,48 @@
#ifndef XRAYPROTOCOLCONFIG_H
#define XRAYPROTOCOLCONFIG_H
#include <QJsonArray>
#include <QJsonObject>
#include "core/utils/constants/protocolConstants.h"
#include <QString>
#include <QStringList>
#include <optional>
namespace amnezia
{
// ── xPadding ─────────────────────────────────────────────────────────────────
struct XrayXPaddingConfig {
QString bytesMin; // xPaddingBytes min
QString bytesMax; // xPaddingBytes max
bool obfsMode = true; // xPaddingObfsMode
QString key; // xPaddingKey
QString header; // xPaddingHeader
QString placement = protocols::xray::defaultXPaddingPlacement; // xPaddingPlacement: Cookie|Header|Query|Body
QString method = protocols::xray::defaultXPaddingMethod; // xPaddingMethod: Repeat-x|Random|Zero
QJsonObject toJson() const;
static XrayXPaddingConfig fromJson(const QJsonObject &json);
};
// ── xmux ─────────────────────────────────────────────────────────────────────
struct XrayXmuxConfig {
bool enabled = true;
QString maxConcurrencyMin = "0";
QString maxConcurrencyMax = "0";
QString maxConnectionsMin = "0";
QString maxConnectionsMax = "0";
QString cMaxReuseTimesMin = "0";
QString cMaxReuseTimesMax = "0";
QString hMaxRequestTimesMin = "0";
QString hMaxRequestTimesMax = "0";
QString hMaxReusableSecsMin = "0";
QString hMaxReusableSecsMax = "0";
QString hKeepAlivePeriod;
QJsonObject toJson() const;
static XrayXmuxConfig fromJson(const QJsonObject &json);
};
struct XrayClientTemplate {
int formatVersion = 0;
QString fingerprint = protocols::xray::defaultFingerprint;
QString uplinkMethod = protocols::xray::defaultXhttpUplinkMethod;
QString uplinkChunkSize = protocols::xray::defaultXhttpUplinkChunkSize;
QString scMinPostsIntervalMsMin = protocols::xray::defaultXhttpScMinPostsIntervalMsMin;
QString scMinPostsIntervalMsMax = protocols::xray::defaultXhttpScMinPostsIntervalMsMax;
XrayXmuxConfig xmux;
QString updatedAt;
QJsonObject toJson() const;
static XrayClientTemplate fromJson(const QJsonObject &json);
QString contentFingerprint() const;
void materializeFromLegacy(const QJsonObject &storedServerJson);
};
// ── XHTTP transport ───────────────────────────────────────────────────────────
struct XrayXhttpConfig {
QString mode = protocols::xray::defaultXhttpMode; // Auto|Packet-up|Stream-up|Stream-one
QString host = protocols::xray::defaultXhttpHost;
QString path;
bool disableGrpc = true;
bool disableSse = true;
// Session & Sequence
QString sessionPlacement = protocols::xray::defaultXhttpSessionPlacement;
QString sessionKey = protocols::xray::defaultXhttpSessionKey;
QString seqPlacement = protocols::xray::defaultXhttpSeqPlacement;
QString seqKey;
QString uplinkDataPlacement = protocols::xray::defaultXhttpUplinkDataPlacement;
QString uplinkDataKey;
// Traffic Shaping
QString scMaxBufferedPosts;
QString scMaxEachPostBytesMin = protocols::xray::defaultXhttpScMaxEachPostBytesMin;
QString scMaxEachPostBytesMax = protocols::xray::defaultXhttpScMaxEachPostBytesMax;
QString scStreamUpServerSecsMin = protocols::xray::defaultXhttpScStreamUpServerSecsMin;
QString scStreamUpServerSecsMax = protocols::xray::defaultXhttpScStreamUpServerSecsMax;
XrayXPaddingConfig xPadding;
QJsonObject toJson() const;
/// Reads only keys present in JSON (no Amnezia UI defaults). Use XrayConfigModel::applyDefaultsToServerConfig for UI.
static XrayXhttpConfig fromJson(const QJsonObject &json);
};
// ── mKCP transport ────────────────────────────────────────────────────────────
struct XrayMkcpConfig {
QString tti;
QString mtu;
QString uplinkCapacity;
QString downlinkCapacity;
QString cwndMultiplier;
QString maxSendingWindow;
QJsonObject toJson() const;
static XrayMkcpConfig fromJson(const QJsonObject &json);
};
/// Which end of the connection the emitted xray json is meant for. The client
/// stream settings are the server ones plus the fields the server never reads.
enum class XrayStreamSide {
Server,
Client,
};
/// Runtime values the server document needs and the structure must not keep:
/// the client list of this particular operation and the ssh-read reality secrets.
struct XrayServerInboundInputs {
QJsonArray clients;
QString realityPrivateKey;
QString realityShortId;
};
/// Why a server.json could not be read back into the structure. The caller turns
/// this into an error code and a log line; parsing itself stays silent.
enum class XrayServerJsonStatus {
Ok,
MissingInbounds,
EmptyInbounds,
MissingStreamSettings,
MissingSettings,
};
/// What to do with account entries that carry no id when the list is rewritten.
enum class XrayClientListFilter {
KeepAll,
DropWithoutId,
};
// ── Server config (settings editable by user) ─────────────────────────────────
struct XrayServerConfig {
QString port;
QString transportProto;
QString subnetAddress;
QString site;
bool isThirdPartyConfig = false;
QString security;
QString flow;
QString sni;
QString alpn;
QString transport;
XrayXhttpConfig xhttp;
XrayMkcpConfig mkcp;
QJsonObject toJson() const;
static XrayServerConfig fromJson(const QJsonObject &json);
void applyDefaults(bool fillFlowDefault = false);
/// Single emitter for both ends. The template is only read on the client side;
/// pass anything on the server side, its fields are not written there.
QJsonObject streamSettingsJson(XrayStreamSide side, const XrayClientTemplate &clientTemplate) const;
QJsonObject serverStreamSettings() const;
QJsonObject clientStreamSettings(const XrayClientTemplate &clientTemplate) const;
/// The whole server.json this configuration means, ready to be uploaded.
QJsonObject toServerInboundJson(const XrayServerInboundInputs &inputs) const;
/// The way back: a server.json read off the container into the two structures.
/// Both are written, because a server document also carries client-side fields
/// (the uTLS preset, the uplink method, xmux) that belong in the template.
static XrayServerJsonStatus fromServerInboundJson(const QJsonObject &serverJson, XrayServerConfig &outServerConfig,
XrayClientTemplate &outClientTemplate);
/// The account list of a server document. Reading is forgiving: a document with
/// no inbounds simply has no accounts. Writing is strict, because putting a list
/// into a document that has nowhere to hold it would drop accounts in silence.
static QJsonArray clientsFromServerInboundJson(const QJsonObject &serverJson);
static XrayServerJsonStatus setClientsInServerInboundJson(QJsonObject &serverJson, const QJsonArray &clients);
/// One account entry, its position in a list, and the flow rewritten across a list.
/// An empty flow means the key is taken out, which is how the raw transport wants it.
static QJsonObject makeClientEntry(const QString &clientId, const QString &flowValue);
static QJsonObject applyFlowToClient(const QJsonObject &client, const QString &flowValue);
static int indexOfClient(const QJsonArray &clients, const QString &clientId);
static QString firstClientId(const QJsonArray &clients);
static QJsonArray applyFlowToClients(const QJsonArray &clients, const QString &flowValue,
XrayClientListFilter filter = XrayClientListFilter::KeepAll);
QJsonObject serverView() const;
QJsonObject issuedConfigView() const;
bool hasEqualServerSettings(const XrayServerConfig &other) const;
QStringList serverViewDifferences(const XrayServerConfig &other) const;
bool breaksIssuedConfigs(const XrayServerConfig &other) const;
static XrayServerConfig fromJson(const QJsonObject& json);
bool hasEqualServerSettings(const XrayServerConfig& other) const;
};
namespace xrayEffective
{
QString xhttpMode(const QString &mode);
QString sessionSeqPlacement(const QString &placement);
QString uplinkDataPlacement(const QString &placement);
QString xPaddingPlacement(const QString &placement);
QString xPaddingMethod(const QString &method);
QString range(const QString &minV, const QString &maxV);
void putRangeIfAny(QJsonObject &obj, const char *key, QString minV, QString maxV, const char *fallbackMin,
const char *fallbackMax);
QString security(const XrayServerConfig &srv);
QString clientFlow(const XrayServerConfig &srv);
QString network(const XrayServerConfig &srv);
QString xhttpModeSent(const XrayServerConfig &srv);
}
/// Runtime values the client document needs and the structures must not keep:
/// the host we connect to, the account this device was given, and the ssh-read
/// reality keys / TLS certificate pin.
struct XrayClientOutboundInputs {
QString serverAddress;
QString clientId;
QString realityPublicKey;
QString realityShortId;
QString tlsPinnedPeerCertSha256;
};
// ── Client config (generated, not edited by user) ─────────────────────────────
struct XrayClientConfig {
QString nativeConfig;
QString localPort;
QString id;
QString templateFingerprint;
QJsonObject toJson() const;
static XrayClientConfig fromJson(const QJsonObject &json);
/// The two runtime values read back out of a native client document.
static QString idFromNativeJson(const QJsonObject &nativeJson);
static QString localPortFromNativeJson(const QJsonObject &nativeJson);
static XrayClientConfig fromJson(const QJsonObject& json);
};
// ── Top-level protocol config ──────────────────────────────────────────────────
struct XrayProtocolConfig {
XrayServerConfig serverConfig;
XrayClientTemplate clientTemplate;
std::optional<XrayClientConfig> clientConfig;
QJsonObject toJson() const;
static XrayProtocolConfig fromJson(const QJsonObject &json);
static XrayProtocolConfig fromJson(const QJsonObject& json);
bool hasClientConfig() const;
void setClientConfig(const XrayClientConfig &config);
void setClientConfig(const XrayClientConfig& config);
void clearClientConfig();
bool needsClientHydration = false;
bool needsTemplateMaterialization = false;
bool templateWasMaterialized = false;
/// The whole client document this configuration means, ready for the core.
QJsonObject toClientOutboundJson(const XrayClientOutboundInputs &inputs) const;
/// The way back: a client document read into the server config, the template and
/// the runtime fields of the cached client config.
bool fromClientOutboundJson(const QJsonObject &nativeJson);
bool hydrateServerConfigFromClientNative();
bool materializeTemplateFromServerConfig();
};
} // namespace amnezia
#endif // XRAYPROTOCOLCONFIG_H

View File

@@ -9,7 +9,6 @@
#include "core/protocols/protocolUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/constants/protocolConstants.h"
#include "core/utils/networkUtilities.h"
namespace amnezia
{
@@ -29,25 +28,6 @@ ContainerConfig NativeServerConfig::containerConfig(DockerContainer container) c
return containers.value(container);
}
void NativeServerConfig::updateContainerConfig(DockerContainer container, const ContainerConfig &config)
{
containers[container] = config;
}
QPair<QString, QString> NativeServerConfig::getDnsPair(const QString &primaryDns, const QString &secondaryDns) const
{
QString d1 = dns1;
QString d2 = dns2;
if (d1.isEmpty() || !NetworkUtilities::checkIPv4Format(d1)) {
d1 = primaryDns;
}
if (d2.isEmpty() || !NetworkUtilities::checkIPv4Format(d2)) {
d2 = secondaryDns;
}
return { d1, d2 };
}
QJsonObject NativeServerConfig::toJson() const
{
QJsonObject obj;
@@ -55,6 +35,9 @@ QJsonObject NativeServerConfig::toJson() const
if (!description.isEmpty()) {
obj[configKey::description] = this->description;
}
if (!displayName.isEmpty()) {
obj[configKey::displayName] = displayName;
}
if (!hostName.isEmpty()) {
obj[configKey::hostName] = hostName;
}
@@ -87,6 +70,7 @@ NativeServerConfig NativeServerConfig::fromJson(const QJsonObject& json)
NativeServerConfig config;
config.description = json.value(configKey::description).toString();
config.displayName = json.value(configKey::displayName).toString();
config.hostName = json.value(configKey::hostName).toString();
QJsonArray containersArray = json.value(configKey::containers).toArray();

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