Compare commits

..

84 Commits

Author SHA1 Message Date
spectrum fa72d8de43 build: make conan tvOS pipeline build
build: make tvOS awg prebuilt explicit

docs: make Apple TV build manual portable
2026-04-27 21:49:00 +03:00
spectrum e43973f8ad feat: add support for tvOS platform 2026-04-25 22:33:27 +03:00
spectrum 8d28beacd8 refactor: separate network extension sources for different protocols and platforms 2026-04-25 22:30:51 +03:00
spectrum 495d59da07 feat: add Apple TV support for network extension and refactor ios cmake scripts 2026-04-25 22:30:07 +03:00
spectrum 528015d17e feat: add support for Apple TV target settings for iOS/Xcode projects 2026-04-25 22:28:06 +03:00
Yaroslav Gurov 55237f951e feat: add conan to all mobile and NE builds 2026-04-21 01:59:18 +02:00
Yaroslav Gurov 1b4f3d5872 chore: remove MacOS-old from build targets 2026-04-21 01:57:47 +02:00
Yaroslav Gurov 8eefe7d26b fix: windows artifact upload 2026-04-20 21:33:59 +02:00
Yaroslav Gurov fd34bf08f1 fix: windows rework CI/CD 2026-04-20 20:40:40 +02:00
Yaroslav Gurov a7734d6093 fix: CI/CD correct QT paths 2026-04-16 16:11:02 +02:00
Yaroslav Gurov 526a7cad89 fix: libncap automake args 2026-04-16 15:31:28 +02:00
Yaroslav Gurov 13955dd36b feat: rework CI/CD for Linux 2026-04-16 14:31:31 +02:00
Yaroslav Gurov d81c1e5c1a feat: rework CI/CD for macos 2026-04-16 01:57:25 +02:00
Yaroslav Gurov 3ad6a7a46d feat: productbuild cpack support 2026-04-13 02:56:18 +02:00
Yaroslav Gurov de5e2635e3 feat: cpack windows full-featured build 2026-04-05 06:18:18 +02:00
Yaroslav Gurov b08154671e feat: linux cpack support 2026-04-03 00:44:35 +02:00
Yaroslav Gurov af2ade1d20 feat: linux initial packaging support 2026-04-02 12:15:41 +02:00
Yaroslav Gurov f456db5392 feat: linux <-> conan features 2026-04-02 12:12:51 +02:00
Yaroslav Gurov ea71e9b87d chore: remove redundant hint from conan find 2026-03-31 14:46:48 +02:00
Yaroslav Gurov 7f3a4aaa14 feat(conan): patch cloak to properly provide env for golang 2026-03-30 17:53:16 +02:00
Yaroslav Gurov 7c455591f6 chore: bump android deps for 16kb support 2026-03-29 03:53:58 +02:00
Yaroslav Gurov f8e229ff7b fix: provide flags for cloak plugin for openvpn-pt-android 2026-03-29 03:53:18 +02:00
Yaroslav Gurov b033fefa31 fix: install for apple systems 2026-03-28 13:41:09 +01:00
Yaroslav Gurov aab0927242 chore: exclude qtkeychain from the target build 2026-03-28 03:08:26 +01:00
Yaroslav Gurov ca8164fde4 feat: support full-featured cmake install 2026-03-28 02:41:49 +01:00
Yaroslav Gurov 7efb532bc9 feat: support of drivers for windows 2026-03-28 02:40:45 +01:00
Yaroslav Gurov 98915b3cf2 chore(conan): recipes cleanup 2026-03-27 00:22:09 +01:00
Yaroslav Gurov fa5076dff0 feat: 16kb support for Android 2026-03-26 23:44:21 +01:00
Yaroslav Gurov dbb918b68c feat: conan and platform bootstrap rework in cmake 2026-03-26 23:43:19 +01:00
Yaroslav Gurov 5078d1a2ca conan: fix version for v2ray-rules-dat 2026-03-26 13:56:53 +01:00
Yaroslav Gurov 6907886c44 conan: make awg-windows and wintun be interpret as exes 2026-03-23 23:37:01 +01:00
Yaroslav Gurov 2f01aac483 conan: windows initial support 2026-03-21 02:03:29 +01:00
Yaroslav Gurov b94cddcb1c conan: address lack of SONAME of libck-ovpn-plugin.so correctly 2026-03-17 21:58:03 +01:00
Yaroslav Gurov 2b5c2cb021 conan: use lib linking instead of QT_EXTRA_LIBS for OVPN 2026-03-17 20:16:36 +01:00
Yaroslav Gurov 729d6ee6d3 conan: add geosite.dat and geoip.dat 2026-03-17 15:22:22 +01:00
Yaroslav Gurov cd87253844 conan: beautify makefile-based recipes 2026-03-17 01:26:00 +01:00
Yaroslav Gurov 8cdaa0c2ee chore: get rid of manual deploy recursive copying 2026-03-17 01:24:24 +01:00
Yaroslav Gurov d563d66b28 chore: bump min macos version 2026-03-17 01:23:14 +01:00
Yaroslav Gurov 37327e30d3 conan: apple full-featured support 2026-03-16 13:24:02 +01:00
NickVs2015 e0af63ce1c fix: awg android connection establish 2026-03-10 00:43:10 +03:00
NickVs2015 041d0fcab5 feat: openvpn add support android 2026-03-09 22:55:05 +03:00
Yaroslav Gurov 2a6960a4fb conan: android additional recipes and fixes 2026-03-09 14:02:47 +01:00
Yaroslav Gurov 8481367fb6 fix: android libssh fixes 2026-03-06 13:29:53 +01:00
Yaroslav Gurov 70d6f9996d feat: conan android initial support 2026-03-06 12:39:46 +01:00
Yaroslav Gurov 616d58e901 feat: macos full feature conan build, except ss and cloak 2026-03-03 01:29:03 +01:00
Yaroslav Gurov 0bfbd82536 feat: add awg-go and awg-apple recipes 2026-02-24 11:51:18 +01:00
Yaroslav Gurov 544ad4ba6e feat: initial conan support 2026-02-23 00:55:11 +01:00
Mitternacht822 02be6dc5f9 chore: add license to msi installer (#2227) 2026-02-20 12:13:08 +08:00
vkamn bfcf7f0305 chore: bump version (#2244) 2026-02-19 20:27:42 +08:00
Mitternacht822 2bce595ade fix: remove revoke from remove subscription flow (#2226)
* fix(revoke): now revoke calls only for unlink device action

* fix: removed revoke call when removing a subscription from the app
2026-02-19 20:23:13 +08:00
Yaroslav Gurov cd1e561fd4 fix: add network watcher back (#2240)
* feat: add reconnect in case of changing network

* fix: reconnect to VPN on wakeup

* fix: linux wakeup build
2026-02-19 20:21:49 +08:00
Mitternacht822 9bd1e6a0f5 fix: added stop and delete AmneziaVPNSplitTunnel on uninstall (#2222) 2026-02-18 11:21:59 +08:00
Yaroslav Gurov 5058c9aa6f fix: do not enable killswitch by default when service starts (#2232) 2026-02-18 10:59:16 +08:00
vkamn d78416835c chore: change default i1 value (#2216) 2026-02-13 17:10:10 +08:00
vkamn 40e6c6aae3 feat: native wg with obfuscation (#2209)
* chore: change default i1 value

* feat: add i1 to native wg with obfuscation
2026-02-12 11:34:52 +08:00
Yaroslav Gurov 911a999c64 fix: xray stability and split-tunneling (#2187)
* fix: xray heap corruption

* fix: use proper configuration for split-tunneled apps

* chore: enable killswitch

* chore: xray windows split-tunneling cleanup

* chore: proper xray killswitch log

* feat: add wait for the tun device

* chore: update amnezia_xray deps for macos

* fix: add nullptr check for split-tunnel on win

* fix: modernize vpnAdapter grabbing function

* fix: remove network watcher due to its fragileness

* chore: xrayprotocol cleanup

* fix: correct wrong iface index on win

* chore: move tun2socks implementation to the client from the service

* chore: xrayprotocol cleanup

* chore: more xrayprotocol cleanup

* fix: consistent tun device with GUID specified

* chore: tun2socks logs

* chore: PrivilegedProcess cleanup
* better error handling in establishment phase
* terminate&kill ops for remote process

* fix: straighforward killing the process on windows

* fix: finally remove GUID setting from tun2socks due to instability

* fix: add sanitizer to ipc process

* chore: do not collect sensitive info from tun2socks
2026-02-11 23:47:28 +08:00
MrMirDan b4f4184aa6 fix: returned mentioned lines (#2205) 2026-02-11 23:44:11 +08:00
NickVs2015 5c6db4b7a4 fix OpenGl error (#2185) 2026-02-10 11:15:31 +08:00
vkamn f6277cdbb2 fix: native wg obfuscation (#2199)
* chore: bump version

* fix: fixed native wg obfuscation
2026-02-09 10:54:30 +08:00
NickVs2015 99312e61d3 fix: allow start Gamepad only Android (#2198) 2026-02-09 10:40:48 +08:00
NickVs2015 9f0ae75a2f feat: add gamepad buttons support android (#2066)
* feat: add support gamepad buttons

* feat: add support gamepad with github repo

* feat: add gitmodules dependency

* feat: add submodule qtgamepad

* chore: update qtgamepad submodule to commit 4e57142e563b931766056b4c7507c16892260222

* fix: update qtgamepad with standard CMake and private headers support

Update qtgamepad to commit f72b3e0 which:
- Replaces qt_add_library with standard add_library to avoid Qt 6.10 macro conflicts
- Copies private headers to build include tree for Android backend
- Creates Qt:: and Qt6:: namespace aliases for proper linking
2026-02-05 22:57:15 +08:00
vkamn 7960d8015d feat: add EULA and policy on IAP page (#2189) 2026-02-05 20:23:06 +08:00
vkamn 5dcc64e5e5 fix: deploy qopensslbackend on windows (#2190) 2026-02-05 20:22:47 +08:00
MrMirDan 964436ad43 fix: placeholder color, hide button image transparency, removed some lines (#2123)
* fix: placeholder color, hide button image transparency, removed unneccessary lines

* update: removed opacity on tunneling page

* update: remove opacity on app tunneling page
2026-02-05 12:56:41 +08:00
ik 4fc3900fd5 Merge pull request #2184 from amnezia-vpn/chore/add-release-date-upload
chore: add sending of release_date to s3
2026-02-04 12:20:23 +03:00
irvinklause 8f5e42dd61 chore: add sending of release_date to s3 2026-02-04 07:38:44 +00:00
Yaroslav Gurov 24895752c1 fix: added enablePeerTraffic call to xray (#2179)
* fix: add enablePeerTraffic call to xray

* chore: remove unnecessary steps during xray TUN setup phase

* chore: move tun init from tun2socks code to ipcserver

* chore: rework xray routing
* get rid of redundant delays
* check if remote calls are successful

* chore: xray routing fine-tuning

* fix: add service qt deps to deployment build
2026-02-04 12:35:53 +08:00
vkamn 87eccfb4ca fix: fix scrolling on drawers (#2183) 2026-02-04 12:35:17 +08:00
ik a983d0504e fix: add checks for script components to find out where it can fall (#2169) 2026-01-30 14:43:30 +08:00
vkamn d0b8535395 fix: update tag deploy (#2168) 2026-01-30 13:15:50 +08:00
dpamnezia f84480cf56 chore: fix artifacts upload (#1961) 2026-01-30 12:43:21 +08:00
MrMirDan de7a026ec1 fix: change drawer parents interactivity (#2004)
* fix: change drawer parents interactivity

* update: better vars names
2026-01-30 12:42:53 +08:00
MrMirDan a128c7d247 fix: keyboard navigation (#2023)
* fix: self-hosted easy install card

* fix: label double click when enter/return pressed
2026-01-30 12:42:29 +08:00
MrMirDan f316f0e25a feat: news notifications switch (#2126)
* feat: news notifications switch

* update: text changes

* fix: notifications enabled by default
2026-01-30 12:19:50 +08:00
NickVs2015 ea5242e29b fix: fixed cipher selection (#2110) 2026-01-30 12:18:54 +08:00
NickVs2015 b31a62c55f feat: add support open files by atv (#2082) 2026-01-30 12:11:26 +08:00
yyy-amnezia 02e3107a23 feat: implement service kickstart and improve macos post install script (#2131) 2026-01-30 12:05:20 +08:00
lunardunno 1862850108 feat: checking linux kernel version when installing amneziawg-go (#2098)
* Checking Linux kernel version when installing amneziawg-go

print the Linux kernel version to stdOut for subsequent checking by the server controller.

* Add error for old linux kernel

Add error 214 ServerLinuxKernelTooOld

* Add case for old linux kernel

Add case for error 214 ServerLinuxKernelTooOld

* Added kernel check for Awg2

Added Linux kernel version check and introduced corresponding ServerLinuxKernelTooOld error for Awg2.
2026-01-30 12:04:27 +08:00
vkamn f73792844c chore: revoke #2148 (#2160) 2026-01-26 19:39:47 +08:00
Yaroslav Gurov a7199ca6f5 fix: add +x permissions to wireguard-go on linux (#2159) 2026-01-26 19:16:39 +08:00
vkamn 5e757cdd3b chore: bump qt version for linux build (#2157) 2026-01-25 21:35:16 +08:00
vkamn 92af1f3268 chore: runners (#2150)
* chore: change runner for linux and android

* chore: add libsecret to linux build

* chore: bump version
2026-01-23 12:05:31 +08:00
Yaroslav Gurov aad9d6dae2 chore: remove redundant gateway (#2148) 2026-01-22 18:21:15 +08:00
Yaroslav Gurov 423fe3fd4f fix: remove redundant gateway from xrayprotocol (#2147) 2026-01-22 18:03:36 +08:00
221 changed files with 15746 additions and 11248 deletions
+56
View File
@@ -0,0 +1,56 @@
# .github/actions/setup-keychain/action.yml
name: Setup apple keychain
description: Creates and configures a temporary build keychain
inputs:
keychain-path:
description: Name of the keychain
required: true
keychain-password:
description: Temporary keychain password
required: true
app-cert-base64:
description: Base64-encoded P12 app certificate
required: true
app-cert-password:
description: Application certificate password
required: true
installer-cert-base64:
description: Base64-encoded P12 installer certificate
required: true
installer-cert-password:
description: Installer certificate password
required: true
runs:
using: composite
steps:
- name: Create keychain
shell: bash
env:
KEYCHAIN_PATH: ${{ inputs.keychain-path }}
KEYCHAIN_PASSWORD: ${{ inputs.keychain-password }}
APP_CERT_BASE64: ${{ inputs.app-cert-base64 }}
APP_CERT_PASSWORD: ${{ inputs.app-cert-password }}
INSTALLER_CERT_BASE64: ${{ inputs.installer-cert-base64 }}
INSTALLER_CERT_PASSWORD: ${{ inputs.installer-cert-password }}
run: |
set -e
APP_CERT_PATH=$RUNNER_TEMP/DeveloperIdApplicationCertificate.p12
INSTALLER_CERT_PATH=$RUNNER_TEMP/DeveloperIdInstallerCertificate.p12
echo -n "$APP_CERT_BASE64" | base64 --decode -o "$APP_CERT_PATH"
echo -n "$INSTALLER_CERT_BASE64" | base64 --decode -o "$INSTALLER_CERT_PATH"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security default-keychain -s "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "${{ github.action_path }}/DeveloperIDG2CA.cer" -k "$KEYCHAIN_PATH" -A
security import "$APP_CERT_PATH" -k "$KEYCHAIN_PATH" -P "$APP_CERT_PASSWORD" -A -t cert -f pkcs12
security import "$INSTALLER_CERT_PATH" -k "$KEYCHAIN_PATH" -P "$INSTALLER_CERT_PASSWORD" -A -t cert -f pkcs12
security set-key-partition-list -S apple-tool:,apple:,codesign: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security list-keychain -d user -s "$KEYCHAIN_PATH"
+98 -205
View File
@@ -10,10 +10,10 @@ env:
jobs:
Build-Linux-Ubuntu:
runs-on: 4-core
runs-on: android-runner
env:
QT_VERSION: 6.8.3
QT_VERSION: 6.10.1
QIF_VERSION: 4.7
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
@@ -38,7 +38,18 @@ jobs:
set-env: 'true'
aqtversion: '==3.3.0'
py7zrversion: '==0.22.*'
extra: '--base ${{ env.QT_MIRROR }}'
extra: '--base ${{ env.QT_MIRROR }}'
- name: 'Setup python'
uses: actions/setup-python@v6
with:
python-version: 3.14
- name: 'Install conan'
run: pip install "conan==2.26.2"
- name: 'Install system packages'
run: sudo apt-get install libxkbcommon-x11-0 libsecret-1-dev
- name: 'Get sources'
uses: actions/checkout@v4
@@ -46,38 +57,17 @@ jobs:
submodules: 'true'
fetch-depth: 10
- name: 'Get version from CMakeLists.txt'
id: get_version
run: |
VERSION=$(grep 'set(AMNEZIAVPN_VERSION' CMakeLists.txt | sed -E 's/.*AMNEZIAVPN_VERSION ([0-9]+.[0-9]+.[0-9]+.[0-9]+)\)/\1/')
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "Version: $VERSION"
# - name: 'Setup ccache'
# uses: hendrikmuhs/ccache-action@v1.2
- name: 'Build project'
run: |
sudo apt-get install libxkbcommon-x11-0
export QT_BIN_DIR=${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/gcc_64/bin
export QIF_BIN_DIR=${{ runner.temp }}/Qt/Tools/QtInstallerFramework/${{ env.QIF_VERSION }}/bin
bash deploy/build_linux.sh
- name: 'Pack installer'
run: cd deploy && tar -cf AmneziaVPN_Linux_Installer.tar AmneziaVPN_Linux_Installer.bin && zip AmneziaVPN_${VERSION}_linux_x64.tar.zip AmneziaVPN_Linux_Installer.tar
shell: bash
env:
QT_INSTALL_DIR: ${{ runner.temp }}
run: ./deploy/build.sh
- name: 'Upload installer artifact'
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: AmneziaVPN_${{ env.VERSION }}_linux_x64.tar.zip
path: deploy/AmneziaVPN_${{ env.VERSION }}_linux_x64.tar.zip
retention-days: 7
- name: 'Upload unpacked artifact'
uses: actions/upload-artifact@v4
with:
name: AmneziaVPN_Linux_unpacked
path: deploy/AppDir
path: deploy/build/AmneziaVPN-*-Linux.run
archive: false
retention-days: 7
- name: 'Upload translations artifact'
@@ -95,7 +85,6 @@ jobs:
env:
QT_VERSION: 6.10.1
QIF_VERSION: 4.7
BUILD_ARCH: 64
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
@@ -111,17 +100,6 @@ jobs:
submodules: 'true'
fetch-depth: 10
- name: 'Get version from CMakeLists.txt'
id: get_version
shell: bash
run: |
VERSION=$(grep 'set(AMNEZIAVPN_VERSION' CMakeLists.txt | sed -E 's/.*AMNEZIAVPN_VERSION ([0-9]+.[0-9]+.[0-9]+.[0-9]+)\)/\1/')
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "Version: $VERSION"
# - name: 'Setup ccache'
# uses: hendrikmuhs/ccache-action@v1.2
- name: 'Install Qt'
uses: jurplel/install-qt-action@v3
with:
@@ -158,39 +136,34 @@ jobs:
$wixBinDir = Join-Path $env:USERPROFILE ".dotnet\tools"
echo "WIX_BIN_DIR=$wixBinDir" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
- name: 'Setup python'
uses: actions/setup-python@v6
with:
python-version: 3.14
- name: 'Install conan'
run: pip install "conan==2.26.2"
- name: 'Build project'
shell: cmd
env:
QT_INSTALL_DIR: ${{ runner.temp }}
run: |
set BUILD_ARCH=${{ env.BUILD_ARCH }}
set QT_BIN_DIR="${{ runner.temp }}\\Qt\\${{ env.QT_VERSION }}\\msvc2022_64\\bin"
set QIF_BIN_DIR="${{ runner.temp }}\\Qt\\Tools\\QtInstallerFramework\\${{ env.QIF_VERSION }}\\bin"
set WIX_BIN_DIR=%USERPROFILE%\.dotnet\tools
call deploy\\build_windows.bat
set WIX_ROOT_PATH="${{ env.USERPROFILE }}\.dotnet\tools"
deploy\build.bat --installer all
- name: 'Rename Windows installer'
shell: cmd
run: |
copy AmneziaVPN_x${{ env.BUILD_ARCH }}.exe AmneziaVPN_%VERSION%_x64.exe
- name: 'Upload installer artifact'
uses: actions/upload-artifact@v4
- name: 'Upload WIX installer artifact'
uses: actions/upload-artifact@v7
with:
name: AmneziaVPN_${{ env.VERSION }}_x64.exe
path: AmneziaVPN_${{ env.VERSION }}_x64.exe
path: deploy/build/AmneziaVPN-*-win64.msi
archive: false
retention-days: 7
- name: 'Upload MSI installer artifact'
uses: actions/upload-artifact@v4
- name: 'Upload IFW installer artifact'
uses: actions/upload-artifact@v7
with:
name: AmneziaVPN_Windows_MSI_installer
path: AmneziaVPN_x${{ env.BUILD_ARCH }}.msi
retention-days: 7
- name: 'Upload unpacked artifact'
uses: actions/upload-artifact@v4
with:
name: AmneziaVPN_Windows_unpacked
path: deploy\\build_${{ env.BUILD_ARCH }}\\client\\Release
path: deploy/build/AmneziaVPN-*-win64.exe
archive: false
retention-days: 7
# ------------------------------------------------------
@@ -258,11 +231,13 @@ jobs:
submodules: 'true'
fetch-depth: 10
# - name: 'Setup ccache'
# uses: hendrikmuhs/ccache-action@v1.2
- name: 'Setup python'
uses: actions/setup-python@v6
with:
python-version: 3.14
- name: 'Install dependencies'
run: pip install jsonschema jinja2
- name: 'Install deps'
run: pip install "conan==2.26.2" jsonschema jinja2
- name: 'Build project'
run: |
@@ -285,93 +260,6 @@ jobs:
IOS_APP_PROVISIONING_PROFILE: ${{ secrets.IOS_APP_PROVISIONING_PROFILE }}
IOS_NE_PROVISIONING_PROFILE: ${{ secrets.IOS_NE_PROVISIONING_PROFILE }}
# - name: 'Upload appstore .ipa and dSYMs to artifacts'
# uses: actions/upload-artifact@v4
# with:
# name: app-store ipa & dsyms
# path: |
# ${{ github.workspace }}/AmneziaVPN-iOS.ipa
# ${{ github.workspace }}/*.app.dSYM.zip
# retention-days: 7
# ------------------------------------------------------
Build-MacOS-old:
runs-on: macos-latest
env:
# Keep compat with MacOS 10.15 aka Catalina by Qt 6.4
QT_VERSION: 6.4.3
MAC_TEAM_ID: ${{ secrets.MAC_TEAM_ID }}
MAC_APP_CERT_CERT: ${{ secrets.MAC_APP_CERT_CERT }}
MAC_SIGNER_ID: ${{ secrets.MAC_SIGNER_ID }}
MAC_APP_CERT_PW: ${{ secrets.MAC_APP_CERT_PW }}
MAC_INSTALLER_SIGNER_CERT: ${{ secrets.MAC_INSTALLER_SIGNER_CERT }}
MAC_INSTALLER_SIGNER_ID: ${{ secrets.MAC_INSTALLER_SIGNER_ID }}
MAC_INSTALL_CERT_PW: ${{ secrets.MAC_INSTALL_CERT_PW }}
APPLE_DEV_EMAIL: ${{ secrets.APPLE_DEV_EMAIL }}
APPLE_DEV_PASSWORD: ${{ secrets.APPLE_DEV_PASSWORD }}
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
FREE_V2_ENDPOINT: ${{ secrets.FREE_V2_ENDPOINT }}
PREM_V1_ENDPOINT: ${{ secrets.PREM_V1_ENDPOINT }}
steps:
- name: 'Setup xcode'
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '15.4.0'
- name: 'Install Qt'
uses: jurplel/install-qt-action@v3
with:
version: ${{ env.QT_VERSION }}
host: 'mac'
target: 'desktop'
arch: 'clang_64'
modules: 'qtremoteobjects qt5compat qtshadertools'
dir: ${{ runner.temp }}
setup-python: 'true'
set-env: 'true'
extra: '--external 7z --base ${{ env.QT_MIRROR }}'
- name: 'Get sources'
uses: actions/checkout@v4
with:
submodules: 'true'
fetch-depth: 10
# - name: 'Setup ccache'
# uses: hendrikmuhs/ccache-action@v1.2
- name: 'Build project'
run: |
export QT_BIN_DIR="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/macos/bin"
bash deploy/build_macos.sh -n
- name: 'Upload installer artifact'
uses: actions/upload-artifact@v4
with:
name: AmneziaVPN_MacOS_old_installer
path: deploy/build/pkg/AmneziaVPN.pkg
retention-days: 7
- name: 'Upload unpacked artifact'
uses: actions/upload-artifact@v4
with:
name: AmneziaVPN_MacOS_old_unpacked
path: deploy/build/client/AmneziaVPN.app
retention-days: 7
# ------------------------------------------------------
Build-MacOS:
@@ -379,19 +267,8 @@ jobs:
env:
QT_VERSION: 6.10.1
MAC_TEAM_ID: ${{ secrets.MAC_TEAM_ID }}
MAC_APP_CERT_CERT: ${{ secrets.MAC_APP_CERT_CERT }}
MAC_SIGNER_ID: ${{ secrets.MAC_SIGNER_ID }}
MAC_APP_CERT_PW: ${{ secrets.MAC_APP_CERT_PW }}
MAC_INSTALLER_SIGNER_CERT: ${{ secrets.MAC_INSTALLER_SIGNER_CERT }}
MAC_INSTALLER_SIGNER_ID: ${{ secrets.MAC_INSTALLER_SIGNER_ID }}
MAC_INSTALL_CERT_PW: ${{ secrets.MAC_INSTALL_CERT_PW }}
APPLE_DEV_EMAIL: ${{ secrets.APPLE_DEV_EMAIL }}
APPLE_DEV_PASSWORD: ${{ secrets.APPLE_DEV_PASSWORD }}
KEYCHAIN_NAME: "build.keychain"
KEYCHAIN_PASSWORD: ""
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
@@ -420,7 +297,15 @@ jobs:
set-env: 'true'
aqtversion: '==3.3.0'
py7zrversion: '==0.22.*'
extra: '--base ${{ env.QT_MIRROR }}'
extra: '--base ${{ env.QT_MIRROR }}'
- name: 'Setup python'
uses: actions/setup-python@v6
with:
python-version: 3.14
- name: 'Install conan'
run: pip install "conan==2.26.2"
- name: 'Get sources'
uses: actions/checkout@v4
@@ -428,39 +313,34 @@ jobs:
submodules: 'true'
fetch-depth: 10
- name: 'Get version from CMakeLists.txt'
id: get_version
run: |
VERSION=$(grep 'set(AMNEZIAVPN_VERSION' CMakeLists.txt | sed -E 's/.*AMNEZIAVPN_VERSION ([0-9]+.[0-9]+.[0-9]+.[0-9]+)\)/\1/')
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "Version: $VERSION"
# - name: 'Setup ccache'
# uses: hendrikmuhs/ccache-action@v1.2
- name: 'Install certs'
uses: ./.github/actions/setup-keychain
with:
keychain-path: ${{ env.KEYCHAIN_NAME }}
keychain-password: ${{ env.KEYCHAIN_PASSWORD }}
app-cert-base64: ${{ secrets.MAC_APP_CERT_CERT }}
app-cert-password: ${{ secrets.MAC_APP_CERT_PW }}
installer-cert-base64: ${{ secrets.MAC_INSTALLER_SIGNER_CERT }}
installer-cert-password: ${{ secrets.MAC_INSTALL_CERT_PW }}
- name: 'Build project'
run: |
export QT_BIN_DIR="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/macos/bin"
bash deploy/build_macos.sh -n
- name: 'Pack macOS installer'
run: |
cd deploy/build/pkg
zip -r ../../AmneziaVPN_${VERSION}_macos.zip AmneziaVPN.pkg
cd ../../..
env:
QT_INSTALL_DIR: ${{ runner.temp }}
CODESIGN_KEYCHAIN: ${{ env.KEYCHAIN_NAME }}
CODESIGN_SIGNATURE: ${{ secrets.MAC_SIGNER_ID }}
CODESIGN_INSTALLER_KEYCHAIN: ${{ env.KEYCHAIN_NAME }}
CODESIGN_INSTALLER_SIGNATURE: ${{ secrets.MAC_INSTALLER_SIGNER_ID }}
NOTARYTOOL_TEAM_ID: ${{ secrets.MAC_TEAM_ID }}
NOTARYTOOL_EMAIL: ${{ secrets.APPLE_DEV_EMAIL }}
NOTARYTOOL_PASSWORD: ${{ secrets.APPLE_DEV_PASSWORD }}
shell: bash
run: deploy/build.sh
- name: 'Upload installer artifact'
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: AmneziaVPN_${{ env.VERSION }}_macos.zip
path: deploy/AmneziaVPN_${{ env.VERSION }}_macos.zip
retention-days: 7
- name: 'Upload unpacked artifact'
uses: actions/upload-artifact@v4
with:
name: AmneziaVPN_MacOS_unpacked
path: deploy/build/client/AmneziaVPN.app
path: deploy/build/AmneziaVPN-*-Darwin.pkg
archive: false
retention-days: 7
Build-MacOS-NE:
@@ -519,8 +399,13 @@ jobs:
submodules: 'true'
fetch-depth: 10
# - name: 'Setup ccache'
# uses: hendrikmuhs/ccache-action@v1.2
- name: 'Setup python'
uses: actions/setup-python@v6
with:
python-version: 3.14
- name: 'Install conan'
run: pip install "conan==2.26.2"
- name: 'Build project'
run: |
@@ -537,7 +422,7 @@ jobs:
# ------------------------------------------------------
Build-Android:
runs-on: 4-core
runs-on: android-runner
env:
ANDROID_BUILD_PLATFORM: android-36
@@ -652,6 +537,14 @@ jobs:
run: |
echo $KEYSTORE_BASE64 | base64 --decode > android.keystore
- name: 'Setup python'
uses: actions/setup-python@v6
with:
python-version: 3.14
- name: 'Install conan'
run: pip install "conan==2.26.2"
- name: 'Build project'
env:
ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }}
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
- name: Verify git tag
run: |
TAG_NAME=${{ inputs.RELEASE_VERSION }}
CMAKE_TAG=$(grep 'project.*VERSION' CMakeLists.txt | sed -E 's/.* ([0-9]+.[0-9]+.[0-9]+.[0-9]+)$/\1/')
CMAKE_TAG=$(grep 'set(AMNEZIAVPN_VERSION' CMakeLists.txt | sed -E 's/.*AMNEZIAVPN_VERSION ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+).*/\1/')
if [[ "$TAG_NAME" == "$CMAKE_TAG" ]]; then
echo "Git tag ($TAG_NAME) matches CMakeLists.txt version ($CMAKE_TAG)."
else
+3 -4
View File
@@ -1,9 +1,5 @@
# User settings
*.user
# Gateway configs (contains sensitive endpoints)
gateway.json
client/gateway.json
macOSPackage/
AmneziaVPN.dmg
AmneziaVPN.exe
@@ -144,3 +140,6 @@ ios-ne-build.sh
macos-ne-build.sh
macos-signed-build.sh
macos-with-sign-build.sh
DeveloperIdApplicationCertificate.p12
DeveloperIdInstallerCertificate.p12
+4
View File
@@ -14,3 +14,7 @@
[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
branch = 6.6
+15 -37
View File
@@ -1,7 +1,14 @@
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
set(PROJECT AmneziaVPN)
set(AMNEZIAVPN_VERSION 4.8.12.8)
set(AMNEZIAVPN_VERSION 4.8.13.1)
set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
set(CMAKE_PROJECT_TOP_LEVEL_INCLUDES
${CMAKE_SOURCE_DIR}/cmake/platform_settings.cmake
${CMAKE_SOURCE_DIR}/cmake/recipes_bootstrap.cmake
${CMAKE_SOURCE_DIR}/cmake/conan_provider.cmake
CACHE STRING "" FORCE)
project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
DESCRIPTION "AmneziaVPN"
@@ -12,7 +19,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
set(RELEASE_DATE "${CURRENT_DATE}")
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
set(APP_ANDROID_VERSION_CODE 2104)
set(APP_ANDROID_VERSION_CODE 2107)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux")
@@ -24,6 +31,8 @@ elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Android")
set(MZ_PLATFORM_NAME "android")
elseif(${CMAKE_SYSTEM_NAME} STREQUAL "iOS")
set(MZ_PLATFORM_NAME "ios")
elseif(${CMAKE_SYSTEM_NAME} STREQUAL "tvOS")
set(MZ_PLATFORM_NAME "ios")
elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Emscripten")
set(MZ_PLATFORM_NAME "wasm")
endif()
@@ -33,7 +42,7 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if(APPLE)
if(IOS)
if(IOS OR CMAKE_SYSTEM_NAME STREQUAL "tvOS")
set(CMAKE_OSX_ARCHITECTURES "arm64")
elseif(MACOS_NE)
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64")
@@ -44,41 +53,10 @@ endif()
add_subdirectory(client)
if(NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
if(NOT IOS AND NOT ANDROID AND NOT MACOS_NE AND NOT CMAKE_SYSTEM_NAME STREQUAL "tvOS")
add_subdirectory(service)
include(${CMAKE_SOURCE_DIR}/deploy/installer/config.cmake)
endif()
set(AMNEZIA_STAGE_DIR "${CMAKE_BINARY_DIR}/stage")
if(WIN32 AND NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
file(TO_CMAKE_PATH "${AMNEZIA_STAGE_DIR}" AMNEZIA_STAGE_DIR_CMAKE)
set(CPACK_GENERATOR "WIX")
set(CPACK_WIX_VERSION 4)
set(CPACK_PACKAGE_NAME "AmneziaVPN")
set(CPACK_PACKAGE_VENDOR "AmneziaVPN")
set(CPACK_PACKAGE_VERSION ${AMNEZIAVPN_VERSION})
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "AmneziaVPN client")
set(CPACK_PACKAGE_INSTALL_DIRECTORY "AmneziaVPN")
set(CPACK_PACKAGE_DIRECTORY "${CMAKE_BINARY_DIR}")
set(CPACK_PACKAGE_EXECUTABLES "AmneziaVPN" "AmneziaVPN")
set(CPACK_WIX_UPGRADE_GUID "{2D55AC62-96D6-4692-8C05-0D85BBF95485}")
set(CPACK_WIX_PRODUCT_ICON "${CMAKE_SOURCE_DIR}/client/images/app.ico")
# WiX patches
set(_AMNEZIA_WIX_PATCH_SERVICE "${CMAKE_SOURCE_DIR}/deploy/installer/wix/service_install_patch.xml")
set(_AMNEZIA_WIX_PATCH_CLOSE_APP "${CMAKE_SOURCE_DIR}/deploy/installer/wix/close_client_patch.xml")
file(TO_CMAKE_PATH "${_AMNEZIA_WIX_PATCH_SERVICE}" _AMNEZIA_WIX_PATCH_SERVICE_CMAKE)
file(TO_CMAKE_PATH "${_AMNEZIA_WIX_PATCH_CLOSE_APP}" _AMNEZIA_WIX_PATCH_CLOSE_APP_CMAKE)
set(CPACK_WIX_PATCH_FILE "${_AMNEZIA_WIX_PATCH_SERVICE_CMAKE};${_AMNEZIA_WIX_PATCH_CLOSE_APP_CMAKE}")
# WiX v4 Util extension for CloseApplication + namespace for util
set(CPACK_WIX_EXTENSIONS "${CPACK_WIX_EXTENSIONS};WixToolset.Util.wixext")
set(CPACK_WIX_CUSTOM_XMLNS "util=http://wixtoolset.org/schemas/v4/wxs/util")
set(CPACK_INSTALLED_DIRECTORIES "${AMNEZIA_STAGE_DIR_CMAKE};/")
include(CPack)
if ((LINUX AND NOT ANDROID) OR (APPLE AND NOT IOS AND NOT MACOS_NE AND NOT CMAKE_SYSTEM_NAME STREQUAL "tvOS") OR (WIN32))
include(${CMAKE_SOURCE_DIR}/cmake/CPack.cmake)
endif()
+340
View File
@@ -0,0 +1,340 @@
# AmneziaVPN Apple TV Build
This document describes how to build the current branch for Apple TV from the repository root.
The pipeline is:
1. Use a separately built static Qt 6.9.2 for `tvOS`.
2. Let Conan build/provide C/C++ dependencies.
3. Generate an Xcode project with `qt-cmake`.
4. Build the `.app` and embedded Network Extension with `xcodebuild`.
Important:
- Run the project commands from the repository root.
- This is a device build for `appletvos`, not a simulator build.
- `xcodebuild build` produces `.app`.
- `.ipa` is produced later via `archive` and `-exportArchive`.
- The current tvOS Network Extension scope is WireGuard-only.
- The temporary tvOS WireGuard bridge prebuilt is opt-in. The Conan recipe does not contain machine-specific fallback paths.
- Do not initialize or update submodules just for this build. If a clean checkout has empty `client/3rd` folders, pass `AMNEZIA_THIRDPARTY_ROOT` to an already initialized read-only `client/3rd` tree.
## 1. Environment
Set these paths for your machine:
```bash
export REPO_ROOT="$PWD"
export QT_DESKTOP_PREFIX="$HOME/Qt/6.9.2/macos"
export QT_TVOS_SRC="$HOME/Qt_tv/qt-6.9.2-tvos-src"
export QT_TVOS_PREFIX="$HOME/Qt_tv/6.9.2/tvos-device"
export BUILD_DIR="$REPO_ROOT/build-tvos-device-conan"
```
If this checkout does not have initialized `client/3rd` sources, point CMake at an initialized tree:
```bash
export AMNEZIA_THIRDPARTY_ROOT="/path/to/initialized/amnezia/client/3rd"
```
If you are using a temporary prebuilt tvOS WireGuard bridge, point Conan at it explicitly:
```bash
export AMNEZIA_TVOS_AWG_PREBUILT_DIR="/path/to/WireGuardKitGo-appletvos"
export AMNEZIA_TVOS_AWG_VERSION_HEADER_DIR="/path/to/directory/with/wireguard-go-version.h"
```
`AMNEZIA_TVOS_AWG_PREBUILT_DIR` must contain `libwg-go.a`.
`AMNEZIA_TVOS_AWG_VERSION_HEADER_DIR` is optional when `wireguard-go-version.h` lives in the same directory as `libwg-go.a`.
If the env vars are not set, the recipe uses the normal source build path. Rebuilding and publishing the tvOS WireGuard bridge through the registry is a separate task.
## 2. Required Local Tools
Conan must be available:
```bash
uv tool install conan
export PATH="$HOME/.local/bin:$PATH"
conan --version
```
Validated version:
```text
Conan version 2.27.1
```
The build uses Xcode's AppleTVOS SDK:
```bash
xcrun --sdk appletvos --show-sdk-path
```
## 3. Prepare Qt Sources
Do not edit the installed Qt sources in place. Copy them into a separate tvOS fork:
```bash
mkdir -p "$HOME/Qt_tv"
rsync -a "$HOME/Qt/6.9.2/Src/" "$QT_TVOS_SRC/"
```
Recommended for reproducibility:
```bash
cd "$QT_TVOS_SRC"
git init
git add .
git commit -m "Qt 6.9.2 source snapshot"
```
## 4. Apply the Qt tvOS Patchset
Apply the local Qt tvOS patchset to `$QT_TVOS_SRC`.
If you need to recreate the patchset from a fresh copy, compare these files against `$HOME/Qt/6.9.2/Src` and reapply the same changes:
- `qtbase/cmake/QtBaseGlobalTargets.cmake`
- `qtbase/cmake/QtBaseHelpers.cmake`
- `qtbase/cmake/QtBuildPathsHelpers.cmake`
- `qtbase/cmake/QtMkspecHelpers.cmake`
- `qtbase/cmake/QtConfig.cmake.in`
- `qtbase/mkspecs/unsupported/macx-tvos-clang/qplatformdefs.h`
- `qtbase/src/corelib/CMakeLists.txt`
- `qtbase/src/corelib/platform/darwin/qdarwinpermissionplugin_location.mm`
- `qtbase/src/gui/CMakeLists.txt`
- `qtbase/src/widgets/CMakeLists.txt`
- `qtbase/src/network/kernel/qnetworkproxy_darwin.cpp`
- `qtbase/src/testlib/qtestcrashhandler.cpp`
- `qtbase/src/plugins/platforms/ios/qiosapplicationdelegate.mm`
- `qtbase/src/plugins/platforms/ios/qiosscreen.mm`
- `qtbase/src/plugins/platforms/ios/qiostheme.mm`
- `qtbase/src/plugins/platforms/ios/quiview.mm`
- `qtbase/src/plugins/platforms/ios/qiosclipboard.mm`
Recommended after patching:
```bash
git -C "$QT_TVOS_SRC" diff > "$HOME/Qt_tv/qt-6.9.2-tvos.patch"
```
Do not use `QT_APPLE_SDK=appletvos`. The working path is `CMAKE_SYSTEM_NAME=tvOS` with `CMAKE_OSX_SYSROOT=appletvos`.
## 5. Build Qt 6.9.2 for Apple TV
Only the modules required by this project are built.
```bash
mkdir -p /private/tmp/qt6.9.2-tvos-device-build
cd /private/tmp/qt6.9.2-tvos-device-build
"$QT_TVOS_SRC/configure" \
-release -static -appstore-compliant \
-nomake tests -nomake examples \
-submodules qtbase,qtdeclarative,qtshadertools,qtremoteobjects,qtsvg,qt5compat,qttools \
-qt-host-path "$QT_DESKTOP_PREFIX" \
-prefix "$QT_TVOS_PREFIX" \
-- \
-G Ninja \
-DQT_QMAKE_TARGET_MKSPEC=macx-tvos-clang \
-DCMAKE_SYSTEM_NAME=tvOS \
-DCMAKE_OSX_SYSROOT=appletvos \
-DCMAKE_OSX_ARCHITECTURES=arm64 \
-DCMAKE_OSX_DEPLOYMENT_TARGET=17.0 \
-DBUILD_SHARED_LIBS=OFF \
-DQT_NO_APPLE_SDK_MAX_VERSION_CHECK=ON
cmake --build . --parallel 8
cmake --install .
```
Sanity checks:
```bash
"$QT_TVOS_PREFIX/bin/qt-cmake" --version
"$QT_TVOS_PREFIX/bin/qmake" -query QMAKE_XSPEC
```
Expected `QMAKE_XSPEC`:
```text
macx-tvos-clang
```
Return to the repository root after building Qt:
```bash
cd "$REPO_ROOT"
```
## 6. Conan Dependency Behavior
For `CMAKE_SYSTEM_NAME=tvOS`, the project-level Conan graph is intentionally reduced:
- included: `awg-apple/2.0.1`
- included: `libssh/0.11.3@amnezia`
- included: `openssl/3.6.1` with `no_apps=True`
- excluded: `openvpnadapter`
- excluded: `hev-socks5-tunnel`
This keeps the current Apple TV target in the same practical scope as before: app plus WireGuard-only Network Extension.
`libssh` is built with `WITH_EXEC=OFF` on tvOS because tvOS does not provide `fork()` or `execv()`.
## 7. Configure the Project
From the repository root:
```bash
cd "$REPO_ROOT"
"$QT_TVOS_PREFIX/bin/qt-cmake" \
-B"$BUILD_DIR" \
-GXcode \
-DQT_HOST_PATH="$QT_DESKTOP_PREFIX" \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_SYSTEM_NAME=tvOS \
-DCMAKE_OSX_SYSROOT=appletvos
```
If you need to provide an external initialized `client/3rd` tree:
```bash
"$QT_TVOS_PREFIX/bin/qt-cmake" \
-B"$BUILD_DIR" \
-GXcode \
-DQT_HOST_PATH="$QT_DESKTOP_PREFIX" \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_SYSTEM_NAME=tvOS \
-DCMAKE_OSX_SYSROOT=appletvos \
-DAMNEZIA_THIRDPARTY_ROOT="$AMNEZIA_THIRDPARTY_ROOT"
```
Expected non-fatal configure warnings:
```text
Warning: plug-in QIOSIntegrationPlugin is not known to the current Qt installation.
Warning: plug-in QJpegPlugin is not known to the current Qt installation.
...
```
In this repo those warnings are tolerated because `client/cmake/ios.cmake` also links the static plugin targets explicitly when available.
## 8. Build the Apple TV App
```bash
xcodebuild -quiet \
-project "$BUILD_DIR/AmneziaVPN.xcodeproj" \
-scheme AmneziaVPN \
-configuration RelWithDebInfo \
-sdk appletvos \
CODE_SIGNING_ALLOWED=NO \
build
```
Outputs:
- `$BUILD_DIR/client/RelWithDebInfo-appletvos/AmneziaVPN.app`
- `$BUILD_DIR/client/RelWithDebInfo-appletvos/AmneziaVPN.app/PlugIns/AmneziaVPNNetworkExtension.appex`
Verification:
```bash
file "$BUILD_DIR/client/RelWithDebInfo-appletvos/AmneziaVPN.app/AmneziaVPN"
file "$BUILD_DIR/client/RelWithDebInfo-appletvos/AmneziaVPN.app/PlugIns/AmneziaVPNNetworkExtension.appex/AmneziaVPNNetworkExtension"
lipo -info "$BUILD_DIR/client/RelWithDebInfo-appletvos/AmneziaVPN.app/AmneziaVPN"
lipo -info "$BUILD_DIR/client/RelWithDebInfo-appletvos/AmneziaVPN.app/PlugIns/AmneziaVPNNetworkExtension.appex/AmneziaVPNNetworkExtension"
```
Expected:
```text
Mach-O 64-bit executable arm64
Non-fat file: ... is architecture: arm64
```
Useful plist checks:
```bash
plutil -p "$BUILD_DIR/client/RelWithDebInfo-appletvos/AmneziaVPN.app/Info.plist" | rg 'CFBundleIdentifier|DTPlatformName|UIDeviceFamily|MinimumOSVersion' -C 1
plutil -p "$BUILD_DIR/client/RelWithDebInfo-appletvos/AmneziaVPN.app/PlugIns/AmneziaVPNNetworkExtension.appex/Info.plist" | rg 'CFBundleIdentifier|NSExtension|DTPlatformName|MinimumOSVersion' -C 1
```
Expected:
- `DTPlatformName => appletvos`
- `UIDeviceFamily => 3`
- `MinimumOSVersion => 17.0`
- extension point `com.apple.networkextension.packet-tunnel`
## 9. `.app` vs `.ipa`
This is the normal sequence:
1. `xcodebuild build` -> `.app`
2. `xcodebuild archive` -> `.xcarchive`
3. `xcodebuild -exportArchive` -> `.ipa`
So seeing `.app` after a successful `build` is correct.
## 10. Optional Archive and Export
The commands below are the next step for packaging, but signing and provisioning must be configured first.
Archive:
```bash
xcodebuild \
-project "$BUILD_DIR/AmneziaVPN.xcodeproj" \
-scheme AmneziaVPN \
-configuration RelWithDebInfo \
-sdk appletvos \
-archivePath "$BUILD_DIR/AmneziaVPN-tvos.xcarchive" \
archive
```
Export:
```bash
xcodebuild -exportArchive \
-archivePath "$BUILD_DIR/AmneziaVPN-tvos.xcarchive" \
-exportPath "$BUILD_DIR/export-tvos" \
-exportOptionsPlist /absolute/path/to/ExportOptions.plist
```
The resulting `.ipa` should appear under:
```text
$BUILD_DIR/export-tvos
```
## 11. Known Non-Fatal Warnings
The validated `xcodebuild` still prints warnings that do not break the build:
- missing Swift search path under the active Xcode Metal toolchain
- `SDKROOT[sdk=...]` target-level warnings generated by Xcode project export
- Swift conditional compilation flag warnings such as `GROUP_ID="..."`
- asset catalog warnings because the current icon set is still iOS-shaped, not a full tvOS Top Shelf asset set
- Go/WireGuard umbrella-header warnings from the temporary local `libwg-go.a` bridge
- deprecated libssh SCP API warnings in existing app code
- `qt_import_plugins()` warnings shown during configure
If the static platform plugin is not linked correctly, the typical failure is:
- `_OBJC_CLASS_$_QIOSApplicationDelegate`
- `_qt_main_wrapper`
Those are cleanup tasks, not blockers for the current build proof.
## 12. Fast Rebuild Checklist
If everything is already built once:
1. Reuse `$QT_TVOS_PREFIX`
2. Reuse Conan cache under `$HOME/.conan2`
3. Reuse or pass an initialized `AMNEZIA_THIRDPARTY_ROOT`
4. Re-run `qt-cmake` into `$BUILD_DIR`
5. Re-run `xcodebuild -quiet ... build`
Vendored Submodule
+1
Submodule client/3rd/qtgamepad added at f72b3e0c62
+44 -56
View File
@@ -3,6 +3,9 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
set(PROJECT AmneziaVPN)
project(${PROJECT})
set(AMNEZIA_THIRDPARTY_ROOT "${CMAKE_CURRENT_LIST_DIR}/3rd" CACHE PATH "Path to Amnezia client/3rd sources")
get_filename_component(AMNEZIA_THIRDPARTY_CLIENT_ROOT "${AMNEZIA_THIRDPARTY_ROOT}/.." ABSOLUTE)
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
set_property(GLOBAL PROPERTY AUTOGEN_TARGETS_FOLDER "Autogen")
set_property(GLOBAL PROPERTY AUTOMOC_TARGETS_FOLDER "Autogen")
@@ -33,22 +36,7 @@ add_definitions(-DDEV_S3_ENDPOINT="$ENV{DEV_S3_ENDPOINT}")
add_definitions(-DFREE_V2_ENDPOINT="$ENV{FREE_V2_ENDPOINT}")
add_definitions(-DPREM_V1_ENDPOINT="$ENV{PREM_V1_ENDPOINT}")
add_definitions(-DAGW_DNS_SERVER="$ENV{AGW_DNS_SERVER}")
add_definitions(-DAGW_DNS_DOMAIN="$ENV{AGW_DNS_DOMAIN}")
add_definitions(-DAGW_DNS_PRIMARY="$ENV{AGW_DNS_PRIMARY}")
add_definitions(-DAGW_DNS_PORT_UDP="$ENV{AGW_DNS_PORT_UDP}")
add_definitions(-DAGW_DNS_PORT_DOT="$ENV{AGW_DNS_PORT_DOT}")
add_definitions(-DAGW_DNS_PORT_DOH="$ENV{AGW_DNS_PORT_DOH}")
add_definitions(-DAGW_DNS_PORT_DOQ="$ENV{AGW_DNS_PORT_DOQ}")
add_definitions(-DAGW_DNS_DOH_PATH="$ENV{AGW_DNS_DOH_PATH}")
add_definitions(-DAGW_DNS_RETRY_COUNT="$ENV{AGW_DNS_RETRY_COUNT}")
add_definitions(-DAGW_DNS_TIMEOUT_MS="$ENV{AGW_DNS_TIMEOUT_MS}")
if(DEFINED ENV{AGW_INSECURE_SSL} AND NOT "$ENV{AGW_INSECURE_SSL}" STREQUAL "" AND NOT "$ENV{AGW_INSECURE_SSL}" STREQUAL "0")
add_definitions(-DAGW_INSECURE_SSL=1)
endif()
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
if(WIN32 OR (APPLE AND NOT IOS AND NOT CMAKE_SYSTEM_NAME STREQUAL "tvOS") OR (LINUX AND NOT ANDROID))
set(PACKAGES ${PACKAGES} Widgets)
endif()
@@ -61,7 +49,7 @@ set(LIBS ${LIBS}
Qt6::Core5Compat Qt6::Concurrent
)
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
if(WIN32 OR (APPLE AND NOT IOS AND NOT CMAKE_SYSTEM_NAME STREQUAL "tvOS") OR (LINUX AND NOT ANDROID))
set(LIBS ${LIBS} Qt6::Widgets)
endif()
@@ -71,10 +59,9 @@ target_include_directories(${PROJECT} PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>
)
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE AND NOT CMAKE_SYSTEM_NAME STREQUAL "tvOS") OR (LINUX AND NOT ANDROID))
qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_interface.rep)
qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_process_interface.rep)
qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_process_tun2socks.rep)
endif()
qt6_add_resources(QRC ${QRC} ${CMAKE_CURRENT_LIST_DIR}/resources.qrc)
@@ -124,6 +111,7 @@ include_directories(
${CMAKE_CURRENT_LIST_DIR}/../ipc
${CMAKE_CURRENT_LIST_DIR}/../common/logger
${CMAKE_CURRENT_LIST_DIR}
${AMNEZIA_THIRDPARTY_CLIENT_ROOT}
${CMAKE_CURRENT_BINARY_DIR}
)
@@ -191,7 +179,7 @@ if(LINUX AND NOT ANDROID)
link_directories(${CMAKE_CURRENT_LIST_DIR}/platforms/linux)
endif()
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE AND NOT CMAKE_SYSTEM_NAME STREQUAL "tvOS") OR (LINUX AND NOT ANDROID))
add_compile_definitions(AMNEZIA_DESKTOP)
endif()
@@ -199,7 +187,8 @@ if(ANDROID)
include(cmake/android.cmake)
endif()
if(IOS)
if(IOS OR CMAKE_SYSTEM_NAME STREQUAL "tvOS")
option(AMNEZIA_IOS_ENABLE_APPLETV_TARGET "Enable Apple TV target settings for iOS/Xcode projects" OFF)
include(cmake/ios.cmake)
include(cmake/ios-arch-fixup.cmake)
elseif(APPLE AND MACOS_NE)
@@ -212,41 +201,40 @@ endif()
target_link_libraries(${PROJECT} PRIVATE ${LIBS})
target_compile_definitions(${PROJECT} PRIVATE "MZ_$<UPPER_CASE:${MZ_PLATFORM_NAME}>")
# deploy artifacts required to run the application to the debug build folder
if(WIN32)
if("${CMAKE_SIZEOF_VOID_P}" STREQUAL "8")
set(DEPLOY_PLATFORM_PATH "windows/x64")
else()
set(DEPLOY_PLATFORM_PATH "windows/x32")
endif()
elseif(LINUX)
set(DEPLOY_PLATFORM_PATH "linux/client")
elseif(APPLE AND NOT IOS)
set(DEPLOY_PLATFORM_PATH "macos")
endif()
if(NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
add_custom_command(
TARGET ${PROJECT} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E $<IF:$<CONFIG:Debug>,copy_directory,true>
${CMAKE_SOURCE_DIR}/deploy/data/${DEPLOY_PLATFORM_PATH}
$<TARGET_FILE_DIR:${PROJECT}>
COMMAND_EXPAND_LISTS
)
add_custom_command(
TARGET ${PROJECT} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E $<IF:$<CONFIG:Debug>,copy_directory,true>
${CMAKE_SOURCE_DIR}/client/3rd-prebuilt/deploy-prebuilt/${DEPLOY_PLATFORM_PATH}
$<TARGET_FILE_DIR:${PROJECT}>
COMMAND_EXPAND_LISTS
)
endif()
target_sources(${PROJECT} PRIVATE ${SOURCES} ${HEADERS} ${RESOURCES} ${QRC} ${I18NQRC})
qt_finalize_target(${PROJECT})
option(BUILD_TESTS "Build transport integration tests" OFF)
if(BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
# Finalize the executable so Qt can gather/deploy QML modules and plugins correctly (Android needs this).
if(COMMAND qt_import_qml_plugins)
qt_import_qml_plugins(${PROJECT})
endif()
if(COMMAND qt_finalize_executable)
qt_finalize_executable(${PROJECT})
else()
qt_finalize_target(${PROJECT})
endif()
if(NOT IOS AND NOT CMAKE_SYSTEM_NAME STREQUAL "tvOS")
install(TARGETS ${PROJECT}
DESTINATION ${CMAKE_INSTALL_BINDIR}
COMPONENT AmneziaVPN
)
install(FILES $<TARGET_RUNTIME_DLLS:${PROJECT}>
DESTINATION ${CMAKE_INSTALL_BINDIR}
COMPONENT AmneziaVPN
)
set(deploy_tool_options "")
if(WIN32)
set(deploy_tool_options "--force-openssl --force")
endif()
qt_generate_deploy_qml_app_script(
TARGET ${PROJECT}
OUTPUT_SCRIPT QT_DEPLOY_SCRIPT
NO_UNSUPPORTED_PLATFORM_ERROR
DEPLOY_TOOL_OPTIONS ${deploy_tool_options}
)
install(SCRIPT ${QT_DEPLOY_SCRIPT}
COMPONENT AmneziaVPN
)
endif()
+2 -2
View File
@@ -250,7 +250,7 @@ bool AmneziaApplication::parseCommands()
return true;
}
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !defined(MACOS_NE)
void AmneziaApplication::startLocalServer() {
const QString serverName("AmneziaVPNInstance");
QLocalServer::removeServer(serverName);
@@ -271,7 +271,7 @@ void AmneziaApplication::startLocalServer() {
bool AmneziaApplication::eventFilter(QObject *watched, QEvent *event)
{
if (event->type() == QEvent::Close) {
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(Q_OS_TVOS)
quit();
#else
if (m_forceQuit) {
+3 -3
View File
@@ -6,7 +6,7 @@
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QThread>
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(Q_OS_TVOS)
#include <QGuiApplication>
#else
#include <QApplication>
@@ -19,7 +19,7 @@
#define amnApp (static_cast<AmneziaApplication *>(QCoreApplication::instance()))
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(Q_OS_TVOS)
#define AMNEZIA_BASE_CLASS QGuiApplication
#else
#define AMNEZIA_BASE_CLASS QApplication
@@ -37,7 +37,7 @@ public:
void loadFonts();
bool parseCommands();
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !defined(MACOS_NE)
void startLocalServer();
#endif
+2 -2
View File
@@ -1,11 +1,11 @@
[versions]
agp = "8.5.2"
agp = "8.6.1"
kotlin = "1.9.24"
androidx-core = "1.13.1"
androidx-activity = "1.9.1"
androidx-annotation = "1.8.2"
androidx-biometric = "1.2.0-alpha05"
androidx-camera = "1.3.4"
androidx-camera = "1.5.3"
androidx-fragment = "1.8.2"
androidx-security-crypto = "1.1.0-alpha06"
androidx-datastore = "1.1.1"
@@ -26,6 +26,8 @@ import android.os.ParcelFileDescriptor
import android.os.SystemClock
import android.provider.OpenableColumns
import android.provider.Settings
import android.view.InputDevice
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
@@ -88,6 +90,10 @@ class AmneziaActivity : QtActivity() {
private val actionResultHandlers = mutableMapOf<Int, ActivityResultHandler>()
private val permissionRequestHandlers = mutableMapOf<Int, PermissionRequestHandler>()
private var isActivityResumed = false
private var hasWindowFocus = false
private val resumeHandler = Handler(Looper.getMainLooper())
private val vpnServiceEventHandler: Handler by lazy(NONE) {
object : Handler(Looper.getMainLooper()) {
@@ -197,10 +203,7 @@ class AmneziaActivity : QtActivity() {
private fun loadLibs() {
listOf(
"rsapss",
"crypto_3",
"ssl_3",
"ssh"
"rsapss"
).forEach {
loadSharedLibrary(this.applicationContext, it)
}
@@ -260,6 +263,10 @@ class AmneziaActivity : QtActivity() {
}
override fun onStop() {
isActivityResumed = false
hasWindowFocus = false
// Cancel all pending operations when activity stops
resumeHandler.removeCallbacksAndMessages(null)
Log.d(TAG, "Stop Amnezia activity")
doUnbindService()
mainScope.launch {
@@ -271,35 +278,91 @@ class AmneziaActivity : QtActivity() {
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
hasWindowFocus = hasFocus
Log.d(TAG, "Window focus changed: hasFocus=$hasFocus")
// Cancel pending operations if window loses focus
if (!hasFocus) {
resumeHandler.removeCallbacksAndMessages(null)
}
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
val deviceId = event.deviceId
val keyCode = event.keyCode
val pressed = event.action == KeyEvent.ACTION_DOWN
val source = event.source
if (deviceId < 0 && pressed) {
when (keyCode) {
KeyEvent.KEYCODE_BUTTON_A,
KeyEvent.KEYCODE_BUTTON_B,
KeyEvent.KEYCODE_BUTTON_X,
KeyEvent.KEYCODE_BUTTON_Y,
KeyEvent.KEYCODE_BUTTON_START,
KeyEvent.KEYCODE_BUTTON_SELECT,
KeyEvent.KEYCODE_DPAD_CENTER -> {
nativeGamepadKeyEvent(0, keyCode, true)
nativeGamepadKeyEvent(0, keyCode, false)
return true
}
}
}
// Real gamepad events (deviceId >= 0)
if (deviceId >= 0) {
val isGamepad = (source and InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD
val isJoystick = (source and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK
val isDpad = (source and InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD
if (isGamepad || isJoystick || isDpad) {
nativeGamepadKeyEvent(deviceId, keyCode, pressed)
return true
}
}
return super.dispatchKeyEvent(event)
}
private external fun nativeGamepadKeyEvent(deviceId: Int, keyCode: Int, pressed: Boolean)
override fun onPause() {
super.onPause()
isActivityResumed = false
// Cancel all pending operations when activity pauses
resumeHandler.removeCallbacksAndMessages(null)
Log.d(TAG, "Pause Amnezia activity")
}
override fun onResume() {
super.onResume()
/* if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
isActivityResumed = true
Log.d(TAG, "Resume Amnezia activity")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
window.decorView.apply {
invalidate()
postDelayed({
sendTouch(1f, 1f)
resumeHandler.postDelayed({
// Check if activity is still resumed and has focus before executing
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
sendTouch(1f, 1f)
}
}, 100)
postDelayed({
sendTouch(2f, 2f)
resumeHandler.postDelayed({
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
sendTouch(2f, 2f)
}
}, 200)
postDelayed({
requestLayout()
invalidate()
resumeHandler.postDelayed({
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
requestLayout()
invalidate()
}
}, 250)
}
} */
Log.d(TAG, "Resume Amnezia activity")
}
}
private fun configureWindowForEdgeToEdge() {
@@ -362,6 +425,10 @@ class AmneziaActivity : QtActivity() {
}
override fun onDestroy() {
isActivityResumed = false
hasWindowFocus = false
// Cancel all pending operations when activity is destroyed
resumeHandler.removeCallbacksAndMessages(null)
Log.d(TAG, "Destroy Amnezia activity")
unregisterBroadcastReceiver(notificationStateReceiver)
notificationStateReceiver = null
@@ -1,7 +1,10 @@
package org.amnezia.vpn
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
@@ -11,7 +14,25 @@ private const val TAG = "TvFilePicker"
class TvFilePicker : ComponentActivity() {
private val fileChooseResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) {
private val fileChooseResultLauncher = registerForActivityResult(object : ActivityResultContracts.OpenDocument() {
override fun createIntent(context: Context, input: Array<String>): Intent {
val intent = super.createIntent(context, input)
val activitiesToResolveIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()))
} else {
@Suppress("DEPRECATION")
context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
}
if (activitiesToResolveIntent.all {
val name = it.activityInfo.packageName
name.startsWith("com.google.android.tv.frameworkpackagestubs") || name.startsWith("com.android.tv.frameworkpackagestubs")
}) {
throw ActivityNotFoundException()
}
return intent
}
}) {
setResult(RESULT_OK, Intent().apply { data = it })
finish()
}
@@ -31,7 +52,7 @@ class TvFilePicker : ComponentActivity() {
private fun getFile() {
try {
Log.v(TAG, "getFile")
fileChooseResultLauncher.launch("*/*")
fileChooseResultLauncher.launch(arrayOf("*/*"))
} catch (_: ActivityNotFoundException) {
Log.w(TAG, "Activity not found")
setResult(RESULT_CANCELED, Intent().apply { putExtra("activityNotFound", true) })
+32 -80
View File
@@ -1,97 +1,49 @@
set(CLIENT_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/..)
set(AMNEZIA_THIRDPARTY_ROOT "${CLIENT_ROOT_DIR}/3rd" CACHE PATH "Path to Amnezia client/3rd sources")
set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/Modules;${CMAKE_MODULE_PATH}")
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/SortFilterProxyModel)
add_subdirectory(${AMNEZIA_THIRDPARTY_ROOT}/SortFilterProxyModel ${CMAKE_CURRENT_BINARY_DIR}/3rd/SortFilterProxyModel)
set(LIBS ${LIBS} SortFilterProxyModel)
include(${CLIENT_ROOT_DIR}/cmake/QSimpleCrypto.cmake)
include(${CLIENT_ROOT_DIR}/3rd/qrcodegen/qrcodegen.cmake)
set(LIBSSH_ROOT_DIR "${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/libssh/")
set(OPENSSL_ROOT_DIR "${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/openssl/")
set(OPENSSL_LIBRARIES_DIR "${OPENSSL_ROOT_DIR}/lib")
if(WIN32)
set(OPENSSL_INCLUDE_DIR "${OPENSSL_ROOT_DIR}/windows/include")
if("${CMAKE_SIZEOF_VOID_P}" STREQUAL "8")
set(LIBSSH_LIB_PATH "${LIBSSH_ROOT_DIR}/windows/x86_64/ssh.lib")
set(LIBSSH_INCLUDE_DIR "${LIBSSH_ROOT_DIR}/windows/x86_64")
set(OPENSSL_LIB_SSL_PATH "${OPENSSL_ROOT_DIR}/windows/win64/libssl.lib")
set(OPENSSL_LIB_CRYPTO_PATH "${OPENSSL_ROOT_DIR}/windows/win64/libcrypto.lib")
else()
set(LIBSSH_LIB_PATH "${LIBSSH_ROOT_DIR}/windows/x86/ssh.lib")
set(LIBSSH_INCLUDE_DIR "${LIBSSH_ROOT_DIR}/windows/x86")
set(OPENSSL_LIB_SSL_PATH "${OPENSSL_ROOT_DIR}/windows/win32/libssl.lib")
set(OPENSSL_LIB_CRYPTO_PATH "${OPENSSL_ROOT_DIR}/windows/win32/libcrypto.lib")
endif()
elseif(APPLE AND NOT IOS)
if(MACOS_NE)
set(LIBSSH_LIB_PATH "${LIBSSH_ROOT_DIR}/macos/universal2/libssh.a")
set(ZLIB_LIB_PATH "${LIBSSH_ROOT_DIR}/macos/universal2/libz.a")
set(LIBSSH_INCLUDE_DIR "${LIBSSH_ROOT_DIR}/macos/universal2")
else()
set(LIBSSH_LIB_PATH "${LIBSSH_ROOT_DIR}/macos/x86_64/libssh.a")
set(ZLIB_LIB_PATH "${LIBSSH_ROOT_DIR}/macos/x86_64/libz.a")
set(LIBSSH_INCLUDE_DIR "${LIBSSH_ROOT_DIR}/macos/x86_64")
endif()
set(OPENSSL_INCLUDE_DIR "${OPENSSL_ROOT_DIR}/macos/include")
set(OPENSSL_LIB_SSL_PATH "${OPENSSL_ROOT_DIR}/macos/lib/libssl.a")
set(OPENSSL_LIB_CRYPTO_PATH "${OPENSSL_ROOT_DIR}/macos/lib/libcrypto.a")
elseif(IOS)
set(LIBSSH_INCLUDE_DIR "${LIBSSH_ROOT_DIR}/ios/arm64")
set(LIBSSH_LIB_PATH "${LIBSSH_ROOT_DIR}/ios/arm64/libssh.a")
set(ZLIB_LIB_PATH "${LIBSSH_ROOT_DIR}/ios/arm64/libz.a")
set(OPENSSL_INCLUDE_DIR "${OPENSSL_ROOT_DIR}/ios/iphone/include")
set(OPENSSL_LIB_SSL_PATH "${OPENSSL_ROOT_DIR}/ios/iphone/lib/libssl.a")
set(OPENSSL_LIB_CRYPTO_PATH "${OPENSSL_ROOT_DIR}/ios/iphone/lib/libcrypto.a")
elseif(ANDROID)
set(abi ${CMAKE_ANDROID_ARCH_ABI})
set(LIBSSH_INCLUDE_DIR "${LIBSSH_ROOT_DIR}/android/${abi}")
set(LIBSSH_LIB_PATH "${LIBSSH_ROOT_DIR}/android/${abi}/libssh.so")
set(OPENSSL_INCLUDE_DIR "${OPENSSL_ROOT_DIR}/android/include")
set(OPENSSL_LIB_SSL_PATH "${OPENSSL_ROOT_DIR}/android/${abi}/libssl.a")
set(OPENSSL_LIB_CRYPTO_PATH "${OPENSSL_ROOT_DIR}/android/${abi}/libcrypto.a")
set(OPENSSL_LIBRARIES_DIR "${OPENSSL_ROOT_DIR}/android/${abi}")
elseif(LINUX)
set(LIBSSH_INCLUDE_DIR "${LIBSSH_ROOT_DIR}/linux/x86_64")
set(ZLIB_LIB_PATH "${LIBSSH_ROOT_DIR}/linux/x86_64/libz.a")
set(LIBSSH_LIB_PATH "${LIBSSH_ROOT_DIR}/linux/x86_64/libssh.a")
set(OPENSSL_INCLUDE_DIR "${OPENSSL_ROOT_DIR}/linux/include")
set(OPENSSL_LIB_SSL_PATH "${OPENSSL_ROOT_DIR}/linux/x86_64/libssl.a")
set(OPENSSL_LIB_CRYPTO_PATH "${OPENSSL_ROOT_DIR}/linux/x86_64/libcrypto.a")
endif()
file(COPY ${OPENSSL_LIB_SSL_PATH} ${OPENSSL_LIB_CRYPTO_PATH}
DESTINATION ${OPENSSL_LIBRARIES_DIR})
set(OPENSSL_USE_STATIC_LIBS TRUE)
set(LIBS ${LIBS}
${LIBSSH_LIB_PATH}
${ZLIB_LIB_PATH}
)
set(LIBS ${LIBS}
${OPENSSL_LIB_SSL_PATH}
${OPENSSL_LIB_CRYPTO_PATH}
)
include(${AMNEZIA_THIRDPARTY_ROOT}/qrcodegen/qrcodegen.cmake)
add_compile_definitions(_WINSOCKAPI_)
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
set(BUILD_WITH_QT6 ON)
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtkeychain)
add_subdirectory(${AMNEZIA_THIRDPARTY_ROOT}/qtkeychain ${CMAKE_CURRENT_BINARY_DIR}/3rd/qtkeychain EXCLUDE_FROM_ALL)
if(ANDROID)
# Use qtgamepad from amnezia-vpn/qtgamepad repository
# Only if Qt6CorePrivate is available (required by qtgamepad)
find_package(Qt6CorePrivate CONFIG QUIET)
if(Qt6CorePrivate_FOUND)
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtgamepad)
# Link both the C++ module and QML plugin
if(TARGET GamepadLegacy)
target_link_libraries(${PROJECT} PRIVATE GamepadLegacy)
endif()
if(TARGET GamepadLegacyQuickPrivate)
target_link_libraries(${PROJECT} PRIVATE GamepadLegacyQuickPrivate)
endif()
message(STATUS "Gamepad support enabled for Android")
else()
message(STATUS "Qt6CorePrivate not found. Gamepad support disabled for Android.")
endif()
endif()
set(LIBS ${LIBS} qt6keychain)
include_directories(
${OPENSSL_INCLUDE_DIR}
${LIBSSH_INCLUDE_DIR}/include
${LIBSSH_ROOT_DIR}/include
${CLIENT_ROOT_DIR}/3rd/libssh/include
${CLIENT_ROOT_DIR}/3rd/QSimpleCrypto/src/include
${CLIENT_ROOT_DIR}/3rd/qtkeychain/qtkeychain
${AMNEZIA_THIRDPARTY_ROOT}/QSimpleCrypto/src/include
${AMNEZIA_THIRDPARTY_ROOT}/qtkeychain/qtkeychain
${CMAKE_CURRENT_BINARY_DIR}/3rd/qtkeychain
${CMAKE_CURRENT_BINARY_DIR}/3rd/libssh/include
)
find_package(OpenSSL REQUIRED)
list(APPEND LIBS OpenSSL::SSL OpenSSL::Crypto)
find_package(libssh REQUIRED)
list(APPEND LIBS ssh::ssh)
+2 -1
View File
@@ -1,5 +1,6 @@
set(CLIENT_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/..)
set(QSIMPLECRYPTO_DIR ${CLIENT_ROOT_DIR}/3rd/QSimpleCrypto/src)
set(AMNEZIA_THIRDPARTY_ROOT "${CLIENT_ROOT_DIR}/3rd" CACHE PATH "Path to Amnezia client/3rd sources")
set(QSIMPLECRYPTO_DIR ${AMNEZIA_THIRDPARTY_ROOT}/QSimpleCrypto/src)
include_directories(${QSIMPLECRYPTO_DIR})
+10 -14
View File
@@ -42,18 +42,14 @@ set(SOURCES ${SOURCES}
${CMAKE_CURRENT_SOURCE_DIR}/core/installedAppsImageProvider.cpp
)
foreach(abi IN ITEMS ${QT_ANDROID_ABIS})
set_property(TARGET ${PROJECT} PROPERTY QT_ANDROID_EXTRA_LIBS
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/amneziawg/android/${abi}/libwg-go.so
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/android/${abi}/libck-ovpn-plugin.so
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/android/${abi}/libovpn3.so
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/android/${abi}/libovpnutil.so
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/android/${abi}/librsapss.so
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openssl/android/${abi}/libcrypto_3.so
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openssl/android/${abi}/libssl_3.so
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/libssh/android/${abi}/libssh.so
)
endforeach()
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/xray/android/libxray.aar
DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/android/xray/libXray)
find_package(awg-android REQUIRED)
set(LIBS ${LIBS} amnezia::awg-android)
set_property(TARGET ${PROJECT} APPEND PROPERTY QT_ANDROID_EXTRA_LIBS ${AMNEZIA_ANDROID_LIBWG_PATH} ${AMNEZIA_ANDROID_LIBWG_QUICK_PATH})
find_package(amnezia-libxray REQUIRED)
file(COPY ${AMNEZIA_LIBXRAY_PATH} DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/android/xray/libXray)
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})
+3 -1
View File
@@ -39,5 +39,7 @@ while(IOS_TARGETS)
set_target_properties(${TARGET_NAME} PROPERTIES
XCODE_ATTRIBUTE_ARCHS[sdk=iphoneos*] "arm64"
XCODE_ATTRIBUTE_ARCHS[sdk=iphonesimulator*] "x86_64"
XCODE_ATTRIBUTE_ARCHS[sdk=appletvos*] "arm64"
XCODE_ATTRIBUTE_ARCHS[sdk=appletvsimulator*] "arm64"
)
endwhile()
endwhile()
+136 -31
View File
@@ -1,7 +1,19 @@
message("Client iOS build")
set(CMAKE_OSX_DEPLOYMENT_TARGET 13.0)
set(APPLE_PROJECT_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
set(AMNEZIA_IOS_APPLETV ${AMNEZIA_IOS_ENABLE_APPLETV_TARGET})
if(AMNEZIA_IOS_APPLETV)
message("Apple TV target mode is ON")
set(CMAKE_OSX_DEPLOYMENT_TARGET 17.0)
set(QT_NO_SET_DEFAULT_IOS_LAUNCH_SCREEN TRUE)
set(QT_NO_ADD_IOS_LAUNCH_SCREEN_TO_BUNDLE TRUE)
set(IOS_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Info-tvOS.plist.in)
set(IOS_LAUNCHSCREEN_STORYBOARD ${CMAKE_CURRENT_SOURCE_DIR}/ios/app/tvOS/AmneziaVPNLaunchScreen.storyboard)
else()
message("Apple TV target mode is OFF")
set(IOS_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Info.plist.in)
set(IOS_LAUNCHSCREEN_STORYBOARD ${CMAKE_CURRENT_SOURCE_DIR}/ios/app/AmneziaVPNLaunchScreen.storyboard)
endif()
enable_language(OBJC)
enable_language(OBJCXX)
@@ -10,13 +22,23 @@ enable_language(Swift)
find_package(Qt6 REQUIRED COMPONENTS ShaderTools)
set(LIBS ${LIBS} Qt6::ShaderTools)
find_library(FW_AUTHENTICATIONSERVICES AuthenticationServices)
find_library(FW_UIKIT UIKit)
find_library(FW_AVFOUNDATION AVFoundation)
find_library(FW_FOUNDATION Foundation)
find_library(FW_STOREKIT StoreKit)
find_library(FW_USERNOTIFICATIONS UserNotifications)
find_library(FW_NETWORKEXTENSION NetworkExtension)
if(AMNEZIA_IOS_APPLETV)
# Use framework linker flags directly for tvOS to avoid iPhoneOS SDK absolute paths.
set(FW_AUTHENTICATIONSERVICES "-framework AuthenticationServices")
set(FW_UIKIT "-framework UIKit")
set(FW_AVFOUNDATION "-framework AVFoundation")
set(FW_FOUNDATION "-framework Foundation")
set(FW_STOREKIT "-framework StoreKit")
set(FW_USERNOTIFICATIONS "-framework UserNotifications")
else()
find_library(FW_AUTHENTICATIONSERVICES AuthenticationServices)
find_library(FW_UIKIT UIKit)
find_library(FW_AVFOUNDATION AVFoundation)
find_library(FW_FOUNDATION Foundation)
find_library(FW_STOREKIT StoreKit)
find_library(FW_USERNOTIFICATIONS UserNotifications)
find_library(FW_NETWORKEXTENSION NetworkExtension)
endif()
set(LIBS ${LIBS}
${FW_AUTHENTICATIONSERVICES}
@@ -25,9 +47,12 @@ set(LIBS ${LIBS}
${FW_FOUNDATION}
${FW_STOREKIT}
${FW_USERNOTIFICATIONS}
${FW_NETWORKEXTENSION}
)
if(NOT AMNEZIA_IOS_APPLETV)
set(LIBS ${LIBS} ${FW_NETWORKEXTENSION})
endif()
set(HEADERS ${HEADERS}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h
@@ -57,7 +82,7 @@ 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_INFO_PLIST ${IOS_INFO_PLIST}
MACOSX_BUNDLE_ICON_FILE "AppIcon"
MACOSX_BUNDLE_INFO_STRING "AmneziaVPN"
MACOSX_BUNDLE_BUNDLE_NAME "AmneziaVPN"
@@ -66,7 +91,6 @@ set_target_properties(${PROJECT} PROPERTIES
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 "${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 "AmneziaVPN"
@@ -74,13 +98,36 @@ set_target_properties(${PROJECT} PROPERTIES
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 networkextension
)
if(AMNEZIA_IOS_APPLETV)
set_target_properties(${PROJECT} PROPERTIES
XCODE_ATTRIBUTE_SUPPORTED_PLATFORMS "appletvos appletvsimulator"
XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "3"
XCODE_ATTRIBUTE_TVOS_DEPLOYMENT_TARGET "${CMAKE_OSX_DEPLOYMENT_TARGET}"
XCODE_ATTRIBUTE_SDKROOT "appletvos"
XCODE_ATTRIBUTE_SDKROOT[sdk=appletvos*] "appletvos"
XCODE_ATTRIBUTE_SDKROOT[sdk=appletvsimulator*] "appletvsimulator"
XCODE_ATTRIBUTE_LIBRARY_SEARCH_PATHS "$(inherited) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)"
XCODE_ATTRIBUTE_LIBRARY_SEARCH_PATHS[sdk=appletvos*] "$(inherited) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)"
XCODE_ATTRIBUTE_LIBRARY_SEARCH_PATHS[sdk=appletvsimulator*] "$(inherited) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)"
XCODE_ATTRIBUTE_EXCLUDED_LIBRARY_SEARCH_PATHS "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS*.sdk/usr/lib/swift"
XCODE_ATTRIBUTE_EXCLUDED_FRAMEWORK_SEARCH_PATHS "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS*.sdk/System/Library/Frameworks"
)
set_target_properties(${PROJECT} PROPERTIES
QT_IOS_PERMISSIONS ""
)
else()
set_target_properties(${PROJECT} PROPERTIES
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "${CMAKE_CURRENT_SOURCE_DIR}/ios/app/main.entitlements"
XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2"
)
endif()
if(DEFINED DEPLOY)
set_target_properties(${PROJECT} PROPERTIES
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Apple Distribution"
@@ -111,7 +158,61 @@ target_compile_options(${PROJECT} PRIVATE
-DVPN_NE_BUNDLEID=\"${BUILD_IOS_APP_IDENTIFIER}.network-extension\"
)
set(WG_APPLE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/3rd/amneziawg-apple/Sources)
if(AMNEZIA_IOS_APPLETV)
# qscnetworkreachability plugin links IOKit, which is unavailable on tvOS.
qt_import_plugins(${PROJECT}
NO_DEFAULT
INCLUDE
QIOSIntegrationPlugin
QJpegPlugin
QSvgPlugin
QGifPlugin
QICOPlugin
QSvgIconPlugin
QSecureTransportBackendPlugin
EXCLUDE
QSCNetworkReachabilityNetworkInformationPlugin
QDarwinCameraPermissionPlugin
)
# Static tvOS Qt build doesn't auto-link these plugin archives into the
# Xcode target, but the app entry point lives in QIOSIntegrationPlugin.
set(_amnezia_tvos_static_plugins
Qt6::QIOSIntegrationPlugin
Qt6::QIOSIntegrationPlugin_init
Qt6::QJpegPlugin
Qt6::QJpegPlugin_init
Qt6::QSvgPlugin
Qt6::QSvgPlugin_init
Qt6::QGifPlugin
Qt6::QGifPlugin_init
Qt6::QICOPlugin
Qt6::QICOPlugin_init
Qt6::QSvgIconPlugin
Qt6::QSvgIconPlugin_init
Qt6::QSecureTransportBackendPlugin
Qt6::QSecureTransportBackendPlugin_init
)
foreach(_amnezia_tvos_static_plugin IN LISTS _amnezia_tvos_static_plugins)
if(TARGET ${_amnezia_tvos_static_plugin})
target_link_libraries(${PROJECT} PRIVATE ${_amnezia_tvos_static_plugin})
endif()
endforeach()
unset(_amnezia_tvos_static_plugin)
unset(_amnezia_tvos_static_plugins)
# Qt 6.9.2 iOS package links IOKit via Qt6::Core interface, but tvOS SDK
# does not provide IOKit. Strip this single framework for Apple TV builds.
get_target_property(_qtcore_iface_libs Qt6::Core INTERFACE_LINK_LIBRARIES)
if(_qtcore_iface_libs)
string(REPLACE "-framework IOKit;" "" _qtcore_iface_libs "${_qtcore_iface_libs}")
string(REPLACE ";-framework IOKit" "" _qtcore_iface_libs "${_qtcore_iface_libs}")
set_property(TARGET Qt6::Core PROPERTY INTERFACE_LINK_LIBRARIES "${_qtcore_iface_libs}")
endif()
endif()
set(AMNEZIA_THIRDPARTY_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/3rd" CACHE PATH "Path to Amnezia client/3rd sources")
set(WG_APPLE_SOURCE_DIR ${AMNEZIA_THIRDPARTY_ROOT}/amneziawg-apple/Sources)
target_sources(${PROJECT} PRIVATE
# ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosvpnprotocol.swift
@@ -123,25 +224,29 @@ target_sources(${PROJECT} PRIVATE
${CLIENT_ROOT_DIR}/platforms/ios/VPNCController.swift
)
target_sources(${PROJECT} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/AmneziaVPNLaunchScreen.storyboard
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Media.xcassets
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
)
if(IOS_LAUNCHSCREEN_STORYBOARD)
target_sources(${PROJECT} PRIVATE
${IOS_LAUNCHSCREEN_STORYBOARD}
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Media.xcassets
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
)
set_property(TARGET ${PROJECT} APPEND PROPERTY RESOURCE
${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
${IOS_LAUNCHSCREEN_STORYBOARD}
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Media.xcassets
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
)
else()
target_sources(${PROJECT} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Media.xcassets
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
)
set_property(TARGET ${PROJECT} APPEND PROPERTY RESOURCE
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/Media.xcassets
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
)
endif()
add_subdirectory(ios/networkextension)
add_dependencies(${PROJECT} networkextension)
set_property(TARGET ${PROJECT} PROPERTY XCODE_EMBED_FRAMEWORKS
"${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios/OpenVPNAdapter.framework"
)
set(CMAKE_XCODE_ATTRIBUTE_FRAMEWORK_SEARCH_PATHS ${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios/)
target_link_libraries("networkextension" PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios/OpenVPNAdapter.framework")
-3
View File
@@ -23,9 +23,6 @@ set_target_properties(${PROJECT} PROPERTIES
MACOSX_BUNDLE_SHORT_VERSION_STRING "${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}"
MACOSX_BUNDLE_BUNDLE_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
)
set(CMAKE_OSX_ARCHITECTURES "x86_64" CACHE INTERNAL "" FORCE)
set(CMAKE_OSX_DEPLOYMENT_TARGET 10.15)
set(HEADERS ${HEADERS}
${CMAKE_CURRENT_SOURCE_DIR}/ui/macos_util.h
-14
View File
@@ -1,7 +1,6 @@
message("Client ==> MacOS NE build")
set_target_properties(${PROJECT} PROPERTIES MACOSX_BUNDLE TRUE)
set(CMAKE_OSX_DEPLOYMENT_TARGET 10.15)
set(APPLE_PROJECT_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
@@ -152,19 +151,6 @@ message(${QtCore_location})
get_filename_component(QT_BIN_DIR_DETECTED "${QtCore_location}/../../../../../bin" ABSOLUTE)
set_property(TARGET ${PROJECT} PROPERTY XCODE_EMBED_FRAMEWORKS
"${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-macos/OpenVPNAdapter.framework"
)
set(CMAKE_XCODE_ATTRIBUTE_FRAMEWORK_SEARCH_PATHS ${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-macos)
target_link_libraries("AmneziaVPNNetworkExtension" PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-macos/OpenVPNAdapter.framework")
add_custom_command(TARGET ${PROJECT} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory
$<TARGET_BUNDLE_DIR:AmneziaVPN>/Contents/Frameworks
COMMAND /usr/bin/find "$<TARGET_BUNDLE_DIR:AmneziaVPN>/Contents/Frameworks/OpenVPNAdapter.framework" -name "*.sha256" -delete
COMMAND /usr/bin/codesign --force --sign "Apple Distribution"
"$<TARGET_BUNDLE_DIR:AmneziaVPN>/Contents/Frameworks/OpenVPNAdapter.framework/Versions/Current/OpenVPNAdapter"
COMMAND ${QT_BIN_DIR_DETECTED}/macdeployqt $<TARGET_BUNDLE_DIR:AmneziaVPN> -appstore-compliant -qmldir=${CMAKE_CURRENT_SOURCE_DIR}
COMMENT "Signing OpenVPNAdapter framework"
)
+4 -17
View File
@@ -23,12 +23,6 @@ set(HEADERS ${HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/version.h
${CLIENT_ROOT_DIR}/core/sshclient.h
${CLIENT_ROOT_DIR}/core/networkUtilities.h
${CLIENT_ROOT_DIR}/core/transport/igatewaytransport.h
${CLIENT_ROOT_DIR}/core/transport/httpGatewayTransport.h
${CLIENT_ROOT_DIR}/core/transport/dnsGatewayTransport.h
${CLIENT_ROOT_DIR}/core/transport/dns/dnsResolver.h
${CLIENT_ROOT_DIR}/core/transport/dns/dnsTunnel.h
${CLIENT_ROOT_DIR}/core/transport/dns/dnsPacket_p.h
${CLIENT_ROOT_DIR}/core/serialization/serialization.h
${CLIENT_ROOT_DIR}/core/serialization/transfer.h
${CLIENT_ROOT_DIR}/../common/logger/logger.h
@@ -45,7 +39,7 @@ set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/mozilla/controllerimpl.h
)
if(NOT IOS AND NOT MACOS_NE)
if(NOT IOS AND NOT MACOS_NE AND NOT CMAKE_SYSTEM_NAME STREQUAL "tvOS")
set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/platforms/ios/QRCodeReaderBase.h
)
@@ -74,11 +68,6 @@ set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/protocols/vpnprotocol.cpp
${CLIENT_ROOT_DIR}/core/sshclient.cpp
${CLIENT_ROOT_DIR}/core/networkUtilities.cpp
${CLIENT_ROOT_DIR}/core/transport/httpGatewayTransport.cpp
${CLIENT_ROOT_DIR}/core/transport/dnsGatewayTransport.cpp
${CLIENT_ROOT_DIR}/core/transport/dns/dnsResolver.cpp
${CLIENT_ROOT_DIR}/core/transport/dns/dnsTunnel.cpp
${CLIENT_ROOT_DIR}/core/transport/dns/dnsPacket.cpp
${CLIENT_ROOT_DIR}/core/serialization/outbound.cpp
${CLIENT_ROOT_DIR}/core/serialization/inbound.cpp
${CLIENT_ROOT_DIR}/core/serialization/ss.cpp
@@ -100,14 +89,14 @@ set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/mozilla/shared/leakdetector.cpp
)
if(NOT IOS AND NOT MACOS_NE)
if(NOT IOS AND NOT MACOS_NE AND NOT CMAKE_SYSTEM_NAME STREQUAL "tvOS")
set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/platforms/ios/QRCodeReaderBase.cpp
)
endif()
# Include native macOS platform helpers (dock/status-item)
if(APPLE AND NOT IOS)
if(APPLE AND NOT IOS AND NOT CMAKE_SYSTEM_NAME STREQUAL "tvOS")
list(APPEND HEADERS
${CLIENT_ROOT_DIR}/platforms/macos/macosutils.h
${CLIENT_ROOT_DIR}/platforms/macos/macosstatusicon.h
@@ -186,13 +175,12 @@ if(WIN32)
)
endif()
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE AND NOT CMAKE_SYSTEM_NAME STREQUAL "tvOS") OR (LINUX AND NOT ANDROID))
message("Client desktop build")
add_compile_definitions(AMNEZIA_DESKTOP)
set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/core/ipcclient.h
${CLIENT_ROOT_DIR}/core/privileged_process.h
${CLIENT_ROOT_DIR}/ui/systemtray_notificationhandler.h
${CLIENT_ROOT_DIR}/protocols/openvpnprotocol.h
${CLIENT_ROOT_DIR}/protocols/openvpnovercloakprotocol.h
@@ -205,7 +193,6 @@ if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/core/ipcclient.cpp
${CLIENT_ROOT_DIR}/core/privileged_process.cpp
${CLIENT_ROOT_DIR}/mozilla/localsocketcontroller.cpp
${CLIENT_ROOT_DIR}/ui/systemtray_notificationhandler.cpp
${CLIENT_ROOT_DIR}/protocols/openvpnprotocol.cpp
@@ -1,17 +1,13 @@
#include "openvpn_configurator.h"
#include <QDebug>
#include <QCoreApplication>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcess>
#include <QString>
#include <QTemporaryDir>
#include <QTemporaryFile>
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
#include <QGuiApplication>
#else
#include <QApplication>
#endif
#include "core/networkUtilities.h"
#include "containers/containers_defs.h"
@@ -165,7 +161,7 @@ QString OpenVpnConfigurator::processConfigWithLocalSettings(const QPair<QString,
QString dnsConf = QString("\nscript-security 2\n"
"up %1/update-resolv-conf.sh\n"
"down %1/update-resolv-conf.sh\n")
.arg(qApp->applicationDirPath());
.arg(QCoreApplication::applicationDirPath());
config.append(dnsConf);
#endif
+7 -11
View File
@@ -1,6 +1,7 @@
#include "ssh_configurator.h"
#include <QDebug>
#include <QCoreApplication>
#include <QObject>
#include <QProcess>
#include <QString>
@@ -8,11 +9,6 @@
#include <QTemporaryFile>
#include <QThread>
#include <qtimer.h>
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(MACOS_NE)
#include <QGuiApplication>
#else
#include <QApplication>
#endif
#include "core/server_defs.h"
#include "utilities.h"
@@ -24,7 +20,7 @@ SshConfigurator::SshConfigurator(std::shared_ptr<Settings> settings, const QShar
QString SshConfigurator::convertOpenSShKey(const QString &key)
{
#if !defined(Q_OS_IOS) && !defined(MACOS_NE)
#if !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !defined(MACOS_NE)
QProcess p;
p.setProcessChannelMode(QProcess::MergedChannels);
@@ -70,13 +66,13 @@ QString SshConfigurator::convertOpenSShKey(const QString &key)
// DEAD CODE.
void SshConfigurator::openSshTerminal(const ServerCredentials &credentials)
{
#if !defined(Q_OS_IOS) && !defined(MACOS_NE)
#if !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !defined(MACOS_NE)
QProcess *p = new QProcess();
p->setProcessChannelMode(QProcess::SeparateChannels);
#ifdef Q_OS_WIN
p->setProcessEnvironment(prepareEnv());
p->setProgram(qApp->applicationDirPath() + "\\cygwin\\putty.exe");
p->setProgram(QCoreApplication::applicationDirPath() + "\\cygwin\\putty.exe");
if (credentials.secretData.contains("PRIVATE KEY")) {
// todo: connect by key
@@ -100,10 +96,10 @@ QProcessEnvironment SshConfigurator::prepareEnv()
#ifdef Q_OS_WIN
pathEnvVar.clear();
pathEnvVar.prepend(QDir::toNativeSeparators(QApplication::applicationDirPath()) + "\\cygwin;");
pathEnvVar.prepend(QDir::toNativeSeparators(QApplication::applicationDirPath()) + "\\openvpn;");
pathEnvVar.prepend(QDir::toNativeSeparators(QCoreApplication::applicationDirPath()) + "\\cygwin;");
pathEnvVar.prepend(QDir::toNativeSeparators(QCoreApplication::applicationDirPath()) + "\\openvpn;");
#elif defined(Q_OS_MACX) && !defined(MACOS_NE)
pathEnvVar.prepend(QDir::toNativeSeparators(QApplication::applicationDirPath()) + "/Contents/MacOS");
pathEnvVar.prepend(QDir::toNativeSeparators(QCoreApplication::applicationDirPath()) + "/Contents/MacOS");
#endif
env.insert("PATH", pathEnvVar);
+1 -1
View File
@@ -79,7 +79,7 @@ namespace apiDefs
constexpr QLatin1String adEndpoint("ad_endpoint");
}
const int requestTimeoutMsecs = 30 * 1000; // 30 secs (increased for DNS transport testing)
const int requestTimeoutMsecs = 12 * 1000; // 12 secs
}
#endif // APIDEFS_H
+4 -4
View File
@@ -8,7 +8,7 @@
#include "platforms/android/android_controller.h"
#endif
#if defined(Q_OS_IOS)
#if defined(Q_OS_IOS) || defined(Q_OS_TVOS)
#include "platforms/ios/ios_controller.h"
#include <AmneziaVPN-Swift.h>
#endif
@@ -196,7 +196,7 @@ void CoreController::initAndroidController()
void CoreController::initAppleController()
{
#ifdef Q_OS_IOS
#if defined(Q_OS_IOS) || defined(Q_OS_TVOS)
IosController::Instance()->initialize();
connect(IosController::Instance(), &IosController::importConfigFromOutside, this, [this](QString data) {
emit m_pageController->goToPageHome();
@@ -233,7 +233,7 @@ void CoreController::initSignalHandlers()
void CoreController::initNotificationHandler()
{
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS)
m_notificationHandler.reset(NotificationHandler::create(nullptr));
connect(m_vpnConnection.get(), &VpnConnection::connectionStateChanged, m_notificationHandler.get(),
@@ -248,7 +248,7 @@ void CoreController::initNotificationHandler()
auto* trayHandler = qobject_cast<SystemTrayNotificationHandler*>(m_notificationHandler.get());
connect(this, &CoreController::websiteUrlChanged, trayHandler, &SystemTrayNotificationHandler::updateWebsiteUrl);
#endif
#endif
}
void CoreController::updateTranslator(const QLocale &locale)
+3 -3
View File
@@ -5,7 +5,7 @@
#include <QQmlContext>
#include <QThread>
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS)
#include "ui/systemtray_notificationhandler.h"
#endif
@@ -49,7 +49,7 @@
#include "ui/models/sites_model.h"
#include "ui/models/newsModel.h"
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS)
#include "ui/notificationhandler.h"
#endif
@@ -99,7 +99,7 @@ private:
QSharedPointer<VpnConnection> m_vpnConnection;
QSharedPointer<QTranslator> m_translator;
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS)
QScopedPointer<NotificationHandler> m_notificationHandler;
#endif
+567 -260
View File
@@ -1,22 +1,29 @@
#include "gatewayController.h"
#include <QDebug>
#include <algorithm>
#include <functional>
#include <random>
#include <QCryptographicHash>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMutexLocker>
#include <QSharedPointer>
#include <QThread>
#include <QtConcurrent>
#include <QNetworkReply>
#include <QPromise>
#include <QUrl>
#include "QBlockCipher.h"
#include "QRsa.h"
#include "amnezia_application.h"
#include "core/transport/dnsGatewayTransport.h"
#include "core/transport/httpGatewayTransport.h"
#include "core/api/apiUtils.h"
#include "core/networkUtilities.h"
#include "utilities.h"
#ifdef AMNEZIA_DESKTOP
#include "core/ipcclient.h"
#endif
namespace
{
namespace configKey
@@ -29,330 +36,630 @@ namespace
constexpr char keyPayload[] = "key_payload";
}
amnezia::transport::dns::DnsProtocol dnsProtocolFromPrimary(PrimaryTransport p)
{
switch (p) {
case PrimaryTransport::DnsUdp: return amnezia::transport::dns::DnsProtocol::Udp;
case PrimaryTransport::DnsTcp: return amnezia::transport::dns::DnsProtocol::Tcp;
case PrimaryTransport::DnsDot: return amnezia::transport::dns::DnsProtocol::Tls;
case PrimaryTransport::DnsDoh: return amnezia::transport::dns::DnsProtocol::Https;
case PrimaryTransport::DnsDoq: return amnezia::transport::dns::DnsProtocol::Quic;
default: return amnezia::transport::dns::DnsProtocol::Udp;
}
}
} // 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.");
TransportsConfig TransportsConfig::fromJson(const QJsonObject &json)
{
using amnezia::transport::dns::DnsProtocol;
constexpr QLatin1String updateRequestResponsePattern("client version update is required");
TransportsConfig config;
constexpr int httpStatusCodeNotFound = 404;
constexpr int httpStatusCodeConflict = 409;
QString primaryStr = json.value("primary").toString("http").toLower();
if (primaryStr == "http") {
config.primary = PrimaryTransport::Http;
} else if (primaryStr == "dns_udp" || primaryStr == "udp") {
config.primary = PrimaryTransport::DnsUdp;
} else if (primaryStr == "dns_tcp" || primaryStr == "tcp") {
config.primary = PrimaryTransport::DnsTcp;
} else if (primaryStr == "dns_dot" || primaryStr == "dot") {
config.primary = PrimaryTransport::DnsDot;
} else if (primaryStr == "dns_doh" || primaryStr == "doh") {
config.primary = PrimaryTransport::DnsDoh;
} else if (primaryStr == "dns_doq" || primaryStr == "doq") {
config.primary = PrimaryTransport::DnsDoq;
}
config.retryCount = json.value("retry_count").toInt(3);
config.timeoutMs = json.value("timeout_ms").toInt(10000);
if (json.contains("http")) {
QJsonObject httpObj = json["http"].toObject();
config.httpEnabled = httpObj.value("enabled").toBool(true);
config.httpEndpoint = httpObj.value("endpoint").toString();
}
if (json.contains("dns_transports")) {
QJsonArray transportsArray = json["dns_transports"].toArray();
for (const auto &transportVal : transportsArray) {
QJsonObject transportObj = transportVal.toObject();
DnsTransportEntry entry;
entry.server = transportObj.value("server").toString();
entry.domain = transportObj.value("domain").toString();
entry.port = static_cast<quint16>(transportObj.value("port").toInt(15353));
entry.dohPath = transportObj.value("path").toString("/dns-query");
QString typeStr = transportObj.value("type").toString().toLower();
if (typeStr == "udp") {
entry.type = DnsProtocol::Udp;
} else if (typeStr == "tcp") {
entry.type = DnsProtocol::Tcp;
} else if (typeStr == "dot" || typeStr == "tls") {
entry.type = DnsProtocol::Tls;
if (!transportObj.contains("port")) entry.port = 8853;
} else if (typeStr == "doh" || typeStr == "https") {
entry.type = DnsProtocol::Https;
if (!transportObj.contains("port")) entry.port = 443;
} else if (typeStr == "doq" || typeStr == "quic") {
entry.type = DnsProtocol::Quic;
if (!transportObj.contains("port")) entry.port = 8853;
} else {
continue;
}
if (entry.isValid()) {
config.dnsTransports.append(entry);
}
}
}
return config;
constexpr int httpStatusCodeNotImplemented = 501;
}
GatewayController::GatewayController(const QString &gatewayEndpoint,
const bool isDevEnvironment,
const int requestTimeoutMsecs,
const bool isStrictKillSwitchEnabled,
QObject *parent)
GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
const bool isStrictKillSwitchEnabled, QObject *parent)
: QObject(parent),
m_requestTimeoutMsecs(requestTimeoutMsecs),
m_gatewayEndpoint(gatewayEndpoint),
m_isDevEnvironment(isDevEnvironment),
m_requestTimeoutMsecs(requestTimeoutMsecs),
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled)
{
auto httpTransport = std::make_shared<amnezia::transport::HttpGatewayTransport>(
m_gatewayEndpoint, m_isDevEnvironment, m_requestTimeoutMsecs, m_isStrictKillSwitchEnabled);
{
QMutexLocker lock(&m_transportMutex);
m_transport = std::move(httpTransport);
}
}
std::shared_ptr<amnezia::transport::IGatewayTransport> GatewayController::buildTransport(
const TransportsConfig &config, int requestTimeoutMsecs, bool isDevEnvironment, bool isStrictKillSwitchEnabled)
GatewayController::EncryptedRequestData GatewayController::prepareRequest(const QString &endpoint, const QJsonObject &apiPayload)
{
using namespace amnezia::transport;
EncryptedRequestData encRequestData;
encRequestData.errorCode = ErrorCode::NoError;
auto makeHttp = [&](const QString &httpEndpoint) {
return std::make_shared<HttpGatewayTransport>(
httpEndpoint, isDevEnvironment, requestTimeoutMsecs, isStrictKillSwitchEnabled);
};
#ifdef Q_OS_IOS
IosController::Instance()->requestInetAccess();
QThread::msleep(10);
#endif
if (config.primary == PrimaryTransport::Http) {
return makeHttp(config.httpEndpoint);
}
encRequestData.request.setTransferTimeout(m_requestTimeoutMsecs);
encRequestData.request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
encRequestData.request.setRawHeader(QString("X-Client-Request-ID").toUtf8(), QUuid::createUuid().toString(QUuid::WithoutBraces).toUtf8());
encRequestData.request.setUrl(endpoint.arg(m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl));
const auto wantedProtocol = dnsProtocolFromPrimary(config.primary);
for (const auto &entry : config.dnsTransports) {
if (entry.type == wantedProtocol && entry.isValid()) {
return std::make_shared<DnsGatewayTransport>(
entry.type, entry.server, entry.domain, entry.port,
requestTimeoutMsecs, isStrictKillSwitchEnabled, entry.dohPath);
// bypass killSwitch exceptions for API-gateway
#ifdef AMNEZIA_DESKTOP
if (m_isStrictKillSwitchEnabled) {
QString host = QUrl(encRequestData.request.url()).host();
QString ip = NetworkUtilities::getIPAddress(host);
if (!ip.isEmpty()) {
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
QRemoteObjectPendingReply<bool> reply = iface->addKillSwitchAllowedRange(QStringList { ip });
if (!reply.waitForFinished(1000) || !reply.returnValue())
qWarning() << "GatewayController::prepareRequest(): Failed to execute remote addKillSwitchAllowedRange call";
});
}
}
return makeHttp(config.httpEndpoint);
}
void GatewayController::setTransportsConfig(const TransportsConfig &config)
{
if (config.timeoutMs > 0) {
m_requestTimeoutMsecs = config.timeoutMs;
}
if (!config.httpEndpoint.isEmpty()) {
m_gatewayEndpoint = config.httpEndpoint;
}
TransportsConfig effective = config;
if (effective.httpEndpoint.isEmpty()) {
effective.httpEndpoint = m_gatewayEndpoint;
}
auto newTransport = buildTransport(effective, m_requestTimeoutMsecs, m_isDevEnvironment, m_isStrictKillSwitchEnabled);
QString activeName;
{
QMutexLocker lock(&m_transportMutex);
m_transport = std::move(newTransport);
activeName = m_transport ? m_transport->name() : QStringLiteral("none");
}
qDebug() << "[Transport] Active transport set to" << activeName;
}
TransportsConfig GatewayController::buildTransportsConfig()
{
using amnezia::transport::dns::DnsProtocol;
TransportsConfig config;
QString server = QString(AGW_DNS_SERVER).trimmed();
QString domain = QString(AGW_DNS_DOMAIN).trimmed();
if (server.isEmpty() || domain.isEmpty()) {
qDebug() << "[Transport] DNS server/domain not configured, HTTP only";
return config;
}
QString primaryStr = QString(AGW_DNS_PRIMARY).trimmed().toLower();
if (primaryStr == "udp" || primaryStr == "dns_udp") {
config.primary = PrimaryTransport::DnsUdp;
} else if (primaryStr == "tcp" || primaryStr == "dns_tcp") {
config.primary = PrimaryTransport::DnsTcp;
} else if (primaryStr == "dot" || primaryStr == "dns_dot") {
config.primary = PrimaryTransport::DnsDot;
} else if (primaryStr == "doh" || primaryStr == "dns_doh") {
config.primary = PrimaryTransport::DnsDoh;
} else if (primaryStr == "doq" || primaryStr == "dns_doq") {
config.primary = PrimaryTransport::DnsDoq;
} else {
config.primary = PrimaryTransport::Http;
}
int retryCount = QString(AGW_DNS_RETRY_COUNT).trimmed().toInt();
config.retryCount = (retryCount > 0) ? retryCount : 3;
int timeoutMs = QString(AGW_DNS_TIMEOUT_MS).trimmed().toInt();
config.timeoutMs = (timeoutMs > 0) ? timeoutMs : 10000;
config.httpEnabled = true;
auto addTransport = [&](DnsProtocol type, const char *portDefine, quint16 defaultPort,
const QString &dohPath = QString()) {
DnsTransportEntry entry;
entry.type = type;
entry.server = server;
entry.domain = domain;
quint16 port = QString(portDefine).trimmed().toUShort();
entry.port = (port > 0) ? port : defaultPort;
if (!dohPath.isEmpty()) entry.dohPath = dohPath;
config.dnsTransports.append(entry);
};
addTransport(DnsProtocol::Udp, AGW_DNS_PORT_UDP, 5353);
addTransport(DnsProtocol::Tcp, AGW_DNS_PORT_UDP, 5353);
addTransport(DnsProtocol::Tls, AGW_DNS_PORT_DOT, 853);
QString dohPath = QString(AGW_DNS_DOH_PATH).trimmed();
if (dohPath.isEmpty()) dohPath = "/dns-query";
addTransport(DnsProtocol::Https, AGW_DNS_PORT_DOH, 443, dohPath);
addTransport(DnsProtocol::Quic, AGW_DNS_PORT_DOQ, 8853);
qDebug() << "[Transport] Built config from env: server=" << server << "domain=" << domain
<< "transports=" << config.dnsTransports.size() << "primary=" << static_cast<int>(config.primary);
return config;
}
GatewayController::EncryptedRequest GatewayController::encryptRequest(const QJsonObject &apiPayload)
{
EncryptedRequest result;
result.errorCode = amnezia::ErrorCode::NoError;
#endif
QSimpleCrypto::QBlockCipher blockCipher;
result.key = blockCipher.generatePrivateSalt(32);
result.iv = blockCipher.generatePrivateSalt(16);
result.salt = blockCipher.generatePrivateSalt(8);
encRequestData.key = blockCipher.generatePrivateSalt(32);
encRequestData.iv = blockCipher.generatePrivateSalt(32);
encRequestData.salt = blockCipher.generatePrivateSalt(8);
QJsonObject keyPayload;
keyPayload[configKey::aesKey] = QString(result.key.toBase64());
keyPayload[configKey::aesIv] = QString(result.iv.toBase64());
keyPayload[configKey::aesSalt] = QString(result.salt.toBase64());
keyPayload[configKey::aesKey] = QString(encRequestData.key.toBase64());
keyPayload[configKey::aesIv] = QString(encRequestData.iv.toBase64());
keyPayload[configKey::aesSalt] = QString(encRequestData.salt.toBase64());
QByteArray encryptedKeyPayload;
QByteArray encryptedApiPayload;
try {
QSimpleCrypto::QRsa rsa;
EVP_PKEY *publicKey = nullptr;
try {
QByteArray rsaKey = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
rsaKey = rsaKey.trimmed();
rsaKey.replace("\\n", "\n");
QSimpleCrypto::QRsa rsa;
publicKey = rsa.getPublicKeyFromByteArray(rsaKey);
} catch (...) {
Utils::logException();
qCritical() << "error loading public key from environment variables";
result.errorCode = amnezia::ErrorCode::ApiMissingAgwPublicKey;
return result;
encRequestData.errorCode = ErrorCode::ApiMissingAgwPublicKey;
return encRequestData;
}
encryptedKeyPayload = rsa.encrypt(QJsonDocument(keyPayload).toJson(QJsonDocument::Compact),
publicKey, RSA_PKCS1_PADDING);
encryptedKeyPayload = rsa.encrypt(QJsonDocument(keyPayload).toJson(), publicKey, RSA_PKCS1_PADDING);
EVP_PKEY_free(publicKey);
encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(QJsonDocument::Compact),
result.key, result.iv, "", result.salt);
encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(), encRequestData.key, encRequestData.iv,
"", encRequestData.salt);
} catch (...) {
Utils::logException();
qCritical() << "error when encrypting the request body";
result.errorCode = amnezia::ErrorCode::ApiConfigDecryptionError;
return result;
encRequestData.errorCode = ErrorCode::ApiConfigDecryptionError;
return encRequestData;
}
QJsonObject requestBody;
requestBody[configKey::keyPayload] = QString(encryptedKeyPayload.toBase64());
requestBody[configKey::apiPayload] = QString(encryptedApiPayload.toBase64());
result.body = QJsonDocument(requestBody).toJson(QJsonDocument::Compact);
return result;
encRequestData.requestBody = QJsonDocument(requestBody).toJson();
return encRequestData;
}
amnezia::transport::DecryptionResult GatewayController::decryptResponse(const QByteArray &encryptedResponseBody,
const QByteArray &key,
const QByteArray &iv,
const QByteArray &salt) const
GatewayController::DecryptionResult GatewayController::tryDecryptResponseBody(const QByteArray &encryptedResponseBody,
QNetworkReply::NetworkError replyError, const QByteArray &key,
const QByteArray &iv, const QByteArray &salt)
{
amnezia::transport::DecryptionResult result;
result.decrypted = encryptedResponseBody;
result.isOk = false;
if (encryptedResponseBody.isEmpty()) {
return result;
}
DecryptionResult result;
result.decryptedBody = encryptedResponseBody;
result.isDecryptionSuccessful = false;
try {
QSimpleCrypto::QBlockCipher blockCipher;
result.decrypted = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt);
result.isOk = true;
result.decryptedBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt);
result.isDecryptionSuccessful = true;
} catch (...) {
result.decrypted = encryptedResponseBody;
result.isOk = false;
result.decryptedBody = encryptedResponseBody;
result.isDecryptionSuccessful = false;
}
return result;
}
std::shared_ptr<amnezia::transport::IGatewayTransport> GatewayController::currentTransport() const
ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
{
QMutexLocker lock(&m_transportMutex);
return m_transport;
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload);
if (encRequestData.errorCode != ErrorCode::NoError) {
return encRequestData.errorCode;
}
QNetworkReply *reply = amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
QEventLoop wait;
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
QList<QSslError> sslErrors;
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec(QEventLoop::ExcludeUserInputEvents);
QByteArray encryptedResponseBody = reply->readAll();
QString replyErrorString = reply->errorString();
auto replyError = reply->error();
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
reply->deleteLater();
auto decryptionResult =
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
if (sslErrors.isEmpty() && shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
auto requestFunction = [&encRequestData, &encryptedResponseBody](const QString &url) {
encRequestData.request.setUrl(url);
return amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
};
auto replyProcessingFunction = [&encryptedResponseBody, &replyErrorString, &replyError, &httpStatusCode, &sslErrors, &encRequestData,
&decryptionResult, this](QNetworkReply *reply, const QList<QSslError> &nestedSslErrors) {
encryptedResponseBody = reply->readAll();
replyErrorString = reply->errorString();
replyError = reply->error();
httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
decryptionResult =
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
if (!sslErrors.isEmpty()
|| shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
sslErrors = nestedSslErrors;
return false;
}
return true;
};
auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString("");
bypassProxy(endpoint, serviceType, userCountryCode, requestFunction, replyProcessingFunction);
}
auto errorCode =
apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, decryptionResult.decryptedBody);
if (errorCode) {
return errorCode;
}
if (!decryptionResult.isDecryptionSuccessful) {
qCritical() << "error when decrypting the request body";
return ErrorCode::ApiConfigDecryptionError;
}
responseBody = decryptionResult.decryptedBody;
return ErrorCode::NoError;
}
amnezia::ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload)
{
EncryptedRequest enc = encryptRequest(apiPayload);
if (enc.errorCode != amnezia::ErrorCode::NoError) {
return enc.errorCode;
auto promise = QSharedPointer<QPromise<QPair<ErrorCode, QByteArray>>>::create();
promise->start();
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload);
if (encRequestData.errorCode != ErrorCode::NoError) {
promise->addResult(qMakePair(encRequestData.errorCode, QByteArray()));
promise->finish();
return promise->future();
}
auto transport = currentTransport();
if (!transport) {
return amnezia::ErrorCode::AmneziaServiceConnectionFailed;
QNetworkReply *reply = amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
auto sslErrors = QSharedPointer<QList<QSslError>>::create();
connect(reply, &QNetworkReply::sslErrors, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
connect(reply, &QNetworkReply::finished, reply, [promise, sslErrors, encRequestData, endpoint, apiPayload, reply, this]() mutable {
QByteArray encryptedResponseBody = reply->readAll();
QString replyErrorString = reply->errorString();
auto replyError = reply->error();
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
reply->deleteLater();
auto decryptionResult =
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
auto processResponse = [promise, encRequestData](const GatewayController::DecryptionResult &decryptionResult,
const QList<QSslError> &sslErrors, QNetworkReply::NetworkError replyError,
const QString &replyErrorString, int httpStatusCode) {
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode,
decryptionResult.decryptedBody);
if (errorCode) {
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();
return;
}
promise->addResult(qMakePair(ErrorCode::NoError, decryptionResult.decryptedBody));
promise->finish();
};
if (sslErrors->isEmpty() && shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString("");
QStringList baseUrls;
if (m_isDevEnvironment) {
baseUrls = QString(DEV_S3_ENDPOINT).split(", ");
} else {
baseUrls = QString(PROD_S3_ENDPOINT).split(", ");
}
QStringList proxyStorageUrls;
if (!serviceType.isEmpty()) {
for (const auto &baseUrl : baseUrls) {
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
proxyStorageUrls.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)
+ ".json");
}
}
for (const auto &baseUrl : baseUrls)
proxyStorageUrls.push_back(baseUrl + "endpoints.json");
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,
const QList<QSslError> &sslErrors, QNetworkReply::NetworkError replyError,
const QString &replyErrorString, int httpStatusCode) {
GatewayController::DecryptionResult result;
result.decryptedBody = decryptedBody;
result.isDecryptionSuccessful = isDecryptionSuccessful;
processResponse(result, sslErrors, replyError, replyErrorString, httpStatusCode);
});
});
});
} else {
processResponse(decryptionResult, *sslErrors, replyError, replyErrorString, httpStatusCode);
}
});
return promise->future();
}
QStringList GatewayController::getProxyUrls(const QString &serviceType, const QString &userCountryCode)
{
QNetworkRequest request;
request.setTransferTimeout(m_requestTimeoutMsecs);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QEventLoop wait;
QList<QSslError> sslErrors;
QNetworkReply *reply;
QStringList baseUrls;
if (m_isDevEnvironment) {
baseUrls = QString(DEV_S3_ENDPOINT).split(", ");
} else {
baseUrls = QString(PROD_S3_ENDPOINT).split(", ");
}
auto decryptionHook = [this, key = enc.key, iv = enc.iv, salt = enc.salt](const QByteArray &encrypted) {
return decryptResponse(encrypted, key, iv, salt);
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
QStringList proxyStorageUrls;
if (!serviceType.isEmpty()) {
for (const auto &baseUrl : baseUrls) {
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
proxyStorageUrls.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json");
}
}
for (const auto &baseUrl : baseUrls) {
proxyStorageUrls.push_back(baseUrl + "endpoints.json");
}
for (const auto &proxyStorageUrl : proxyStorageUrls) {
request.setUrl(proxyStorageUrl);
reply = amnApp->networkManager()->get(request);
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec(QEventLoop::ExcludeUserInputEvents);
if (reply->error() == QNetworkReply::NetworkError::NoError) {
auto encryptedResponseBody = reply->readAll();
reply->deleteLater();
EVP_PKEY *privateKey = nullptr;
QByteArray responseBody;
try {
if (!m_isDevEnvironment) {
QCryptographicHash hash(QCryptographicHash::Sha512);
hash.addData(key);
QByteArray hashResult = hash.result().toHex();
QByteArray key = QByteArray::fromHex(hashResult.left(64));
QByteArray iv = QByteArray::fromHex(hashResult.mid(64, 32));
QByteArray ba = QByteArray::fromBase64(encryptedResponseBody);
QSimpleCrypto::QBlockCipher blockCipher;
responseBody = blockCipher.decryptAesBlockCipher(ba, key, iv);
} else {
responseBody = encryptedResponseBody;
}
} catch (...) {
Utils::logException();
qCritical() << "error loading private key from environment variables or decrypting payload" << encryptedResponseBody;
continue;
}
auto endpointsArray = QJsonDocument::fromJson(responseBody).array();
QStringList endpoints;
for (const auto &endpoint : endpointsArray) {
endpoints.push_back(endpoint.toString());
}
return endpoints;
} else {
auto replyError = reply->error();
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
qDebug() << replyError;
qDebug() << httpStatusCode;
qDebug() << "go to the next storage endpoint";
reply->deleteLater();
}
}
return {};
}
bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody,
bool isDecryptionSuccessful)
{
const QByteArray &responseBody = decryptedResponseBody;
int httpStatus = -1;
if (isDecryptionSuccessful) {
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
if (jsonDoc.isObject()) {
QJsonObject jsonObj = jsonDoc.object();
httpStatus = jsonObj.value("http_status").toInt(-1);
}
} else {
qDebug() << "failed to decrypt the data";
return true;
}
if (replyError == QNetworkReply::NetworkError::OperationCanceledError || replyError == QNetworkReply::NetworkError::TimeoutError) {
qDebug() << "timeout occurred";
qDebug() << replyError;
return true;
} else if (responseBody.contains("html")) {
qDebug() << "the response contains an html tag";
return true;
} else if (httpStatus == httpStatusCodeNotFound) {
if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2)
|| responseBody.contains(errorResponsePattern3)) {
return false;
} else {
qDebug() << replyError;
return true;
}
} else if (httpStatus == httpStatusCodeNotImplemented) {
if (responseBody.contains(updateRequestResponsePattern)) {
return false;
} else {
qDebug() << replyError;
return true;
}
} else if (httpStatus == httpStatusCodeConflict) {
return false;
} else if (replyError != QNetworkReply::NetworkError::NoError) {
qDebug() << replyError;
return true;
}
return false;
}
void GatewayController::bypassProxy(const QString &endpoint, const QString &serviceType, const QString &userCountryCode,
std::function<QNetworkReply *(const QString &url)> requestFunction,
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction)
{
QStringList proxyUrls = getProxyUrls(serviceType, userCountryCode);
std::random_device randomDevice;
std::mt19937 generator(randomDevice());
std::shuffle(proxyUrls.begin(), proxyUrls.end(), generator);
QByteArray responseBody;
auto bypassFunction = [this](const QString &endpoint, const QString &proxyUrl,
std::function<QNetworkReply *(const QString &url)> requestFunction,
std::function<bool(QNetworkReply * reply, const QList<QSslError> &sslErrors)> replyProcessingFunction) {
QEventLoop wait;
QList<QSslError> sslErrors;
qDebug() << "go to the next proxy endpoint";
QNetworkReply *reply = requestFunction(endpoint.arg(proxyUrl));
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec(QEventLoop::ExcludeUserInputEvents);
auto result = replyProcessingFunction(reply, sslErrors);
reply->deleteLater();
return result;
};
return transport->send(endpoint, enc.body, responseBody, decryptionHook);
if (m_proxyUrl.isEmpty()) {
QNetworkRequest request;
request.setTransferTimeout(1000);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QEventLoop wait;
QList<QSslError> sslErrors;
QNetworkReply *reply;
for (const QString &proxyUrl : proxyUrls) {
request.setUrl(proxyUrl + "lmbd-health");
reply = amnApp->networkManager()->get(request);
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec(QEventLoop::ExcludeUserInputEvents);
if (reply->error() == QNetworkReply::NetworkError::NoError) {
reply->deleteLater();
m_proxyUrl = proxyUrl;
if (!m_proxyUrl.isEmpty()) {
break;
}
} else {
reply->deleteLater();
}
}
}
if (!m_proxyUrl.isEmpty()) {
if (bypassFunction(endpoint, m_proxyUrl, requestFunction, replyProcessingFunction)) {
return;
}
}
for (const QString &proxyUrl : proxyUrls) {
if (bypassFunction(endpoint, proxyUrl, requestFunction, replyProcessingFunction)) {
m_proxyUrl = proxyUrl;
break;
}
}
}
QFuture<QPair<amnezia::ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload)
void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
std::function<void(const QStringList &)> onComplete)
{
return QtConcurrent::run([this, endpoint, apiPayload]() {
QByteArray responseBody;
amnezia::ErrorCode errorCode = post(endpoint, apiPayload, responseBody);
return qMakePair(errorCode, responseBody);
if (currentProxyStorageIndex >= proxyStorageUrls.size()) {
onComplete({});
return;
}
QNetworkRequest request;
request.setTransferTimeout(m_requestTimeoutMsecs);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setUrl(proxyStorageUrls[currentProxyStorageIndex]);
QNetworkReply *reply = amnApp->networkManager()->get(request);
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) { *(state->sslErrors) = e; });
connect(reply, &QNetworkReply::finished, this, [this, proxyStorageUrls, currentProxyStorageIndex, onComplete, reply]() {
if (reply->error() == QNetworkReply::NoError) {
QByteArray encrypted = reply->readAll();
reply->deleteLater();
QByteArray responseBody;
try {
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
if (!m_isDevEnvironment) {
QCryptographicHash hash(QCryptographicHash::Sha512);
hash.addData(key);
QByteArray h = hash.result().toHex();
QByteArray decKey = QByteArray::fromHex(h.left(64));
QByteArray iv = QByteArray::fromHex(h.mid(64, 32));
QByteArray ba = QByteArray::fromBase64(encrypted);
QSimpleCrypto::QBlockCipher cipher;
responseBody = cipher.decryptAesBlockCipher(ba, decKey, iv);
} else {
responseBody = encrypted;
}
} catch (...) {
Utils::logException();
qCritical() << "error decrypting payload";
QMetaObject::invokeMethod(
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
return;
}
QJsonArray endpointsArray = QJsonDocument::fromJson(responseBody).array();
QStringList endpoints;
for (const QJsonValue &endpoint : endpointsArray)
endpoints.push_back(endpoint.toString());
QStringList shuffled = endpoints;
std::random_device randomDevice;
std::mt19937 generator(randomDevice());
std::shuffle(shuffled.begin(), shuffled.end(), generator);
onComplete(shuffled);
return;
}
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
qDebug() << httpStatusCode;
qDebug() << "go to the next storage endpoint";
reply->deleteLater();
QMetaObject::invokeMethod(
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
});
}
void GatewayController::getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex,
std::function<void(const QString &)> onComplete)
{
if (currentProxyIndex >= proxyUrls.size()) {
onComplete("");
return;
}
QNetworkRequest request;
request.setTransferTimeout(1000);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setUrl(proxyUrls[currentProxyIndex] + "lmbd-health");
QNetworkReply *reply = amnApp->networkManager()->get(request);
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) {
// *(state->sslErrors) = e;
// });
connect(reply, &QNetworkReply::finished, this, [this, proxyUrls, currentProxyIndex, onComplete, reply]() {
reply->deleteLater();
if (reply->error() == QNetworkReply::NoError) {
m_proxyUrl = proxyUrls[currentProxyIndex];
onComplete(m_proxyUrl);
return;
}
qDebug() << "go to the next proxy endpoint";
QMetaObject::invokeMethod(this, [=]() { getProxyUrlAsync(proxyUrls, currentProxyIndex + 1, onComplete); }, Qt::QueuedConnection);
});
}
void GatewayController::bypassProxyAsync(
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete)
{
auto sslErrors = QSharedPointer<QList<QSslError>>::create();
if (proxyUrl.isEmpty()) {
onComplete(QByteArray(), false, *sslErrors, QNetworkReply::InternalServerError, "empty proxy url", 0);
return;
}
QNetworkRequest request = encRequestData.request;
request.setUrl(endpoint.arg(proxyUrl));
QNetworkReply *reply = amnApp->networkManager()->post(request, encRequestData.requestBody);
connect(reply, &QNetworkReply::sslErrors, this, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
connect(reply, &QNetworkReply::finished, this, [sslErrors, onComplete, encRequestData, reply, this]() {
QByteArray encryptedResponseBody = reply->readAll();
QString replyErrorString = reply->errorString();
auto replyError = reply->error();
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
reply->deleteLater();
auto decryptionResult =
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
onComplete(decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful, *sslErrors, replyError, replyErrorString,
httpStatusCode);
});
}
+34 -52
View File
@@ -2,87 +2,69 @@
#define GATEWAYCONTROLLER_H
#include <QFuture>
#include <QJsonArray>
#include <QJsonObject>
#include <QMutex>
#include <QNetworkReply>
#include <QObject>
#include <QPair>
#include <memory>
#include <QPromise>
#include <QSharedPointer>
#include "core/defs.h"
#include "core/transport/dns/dnsResolver.h"
#include "core/transport/igatewaytransport.h"
struct DnsTransportEntry
{
amnezia::transport::dns::DnsProtocol type = amnezia::transport::dns::DnsProtocol::Udp;
QString server;
QString domain;
quint16 port = 15353;
QString dohPath = "/dns-query";
bool isValid() const { return !server.isEmpty() && !domain.isEmpty(); }
};
enum class PrimaryTransport { Http, DnsUdp, DnsTcp, DnsDot, DnsDoh, DnsDoq };
struct TransportsConfig
{
PrimaryTransport primary = PrimaryTransport::Http;
bool httpEnabled = true;
QString httpEndpoint;
QList<DnsTransportEntry> dnsTransports;
int retryCount = 3;
int timeoutMs = 10000;
bool isValid() const { return httpEnabled || !dnsTransports.isEmpty(); }
static TransportsConfig fromJson(const QJsonObject &json);
};
#ifdef Q_OS_IOS
#include "platforms/ios/ios_controller.h"
#endif
class GatewayController : public QObject
{
Q_OBJECT
public:
explicit GatewayController(const QString &gatewayEndpoint,
const bool isDevEnvironment,
const int requestTimeoutMsecs,
const bool isStrictKillSwitchEnabled,
QObject *parent = nullptr);
explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
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);
static TransportsConfig buildTransportsConfig();
void setTransportsConfig(const TransportsConfig &config);
private:
struct EncryptedRequest
struct EncryptedRequestData
{
QByteArray body;
QNetworkRequest request;
QByteArray requestBody;
QByteArray key;
QByteArray iv;
QByteArray salt;
amnezia::ErrorCode errorCode = amnezia::ErrorCode::NoError;
amnezia::ErrorCode errorCode;
};
EncryptedRequest encryptRequest(const QJsonObject &apiPayload);
amnezia::transport::DecryptionResult decryptResponse(const QByteArray &encryptedResponseBody,
const QByteArray &key,
const QByteArray &iv,
const QByteArray &salt) const;
struct DecryptionResult
{
QByteArray decryptedBody;
bool isDecryptionSuccessful;
};
std::shared_ptr<amnezia::transport::IGatewayTransport> currentTransport() const;
static std::shared_ptr<amnezia::transport::IGatewayTransport> buildTransport(
const TransportsConfig &config, int requestTimeoutMsecs, bool isDevEnvironment, bool isStrictKillSwitchEnabled);
EncryptedRequestData prepareRequest(const QString &endpoint, const QJsonObject &apiPayload);
DecryptionResult tryDecryptResponseBody(const QByteArray &encryptedResponseBody, QNetworkReply::NetworkError replyError,
const QByteArray &key, const QByteArray &iv, const QByteArray &salt);
QStringList getProxyUrls(const QString &serviceType, const QString &userCountryCode);
bool shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody, bool isDecryptionSuccessful);
void bypassProxy(const QString &endpoint, const QString &serviceType, const QString &userCountryCode,
std::function<QNetworkReply *(const QString &url)> requestFunction,
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction);
void getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
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,
std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete);
int m_requestTimeoutMsecs;
QString m_gatewayEndpoint;
bool m_isDevEnvironment = false;
bool m_isStrictKillSwitchEnabled = false;
mutable QMutex m_transportMutex;
std::shared_ptr<amnezia::transport::IGatewayTransport> m_transport;
inline static QString m_proxyUrl;
};
#endif // GATEWAYCONTROLLER_H
@@ -419,6 +419,18 @@ ErrorCode ServerController::installDockerWorker(const ServerCredentials &credent
cbReadStdOut, cbReadStdErr);
qDebug().noquote() << "ServerController::installDockerWorker" << stdOut;
if (container == DockerContainer::Awg2) {
QRegularExpression regex(R"(Linux\s+(\d+)\.(\d+)[^\d]*)");
QRegularExpressionMatch match = regex.match(stdOut);
if (match.hasMatch()) {
int majorVersion = match.captured(1).toInt();
int minorVersion = match.captured(2).toInt();
if (majorVersion < 4 || (majorVersion == 4 && minorVersion < 14)) {
return ErrorCode::ServerLinuxKernelTooOld;
}
}
}
if (stdOut.contains("lock"))
return ErrorCode::ServerPacketManagerError;
if (stdOut.contains("command not found"))
+1
View File
@@ -61,6 +61,7 @@ namespace amnezia
ServerDockerOnCgroupsV2 = 211,
ServerCgroupMountpoint = 212,
DockerPullRateLimit = 213,
ServerLinuxKernelTooOld = 214,
// Ssh connection errors
SshRequestDeniedError = 300,
+1
View File
@@ -29,6 +29,7 @@ QString errorString(ErrorCode code) {
case(ErrorCode::ServerDockerOnCgroupsV2): errorMessage = QObject::tr("Docker error: runc doesn't work on cgroups v2"); break;
case(ErrorCode::ServerCgroupMountpoint): errorMessage = QObject::tr("Server error: cgroup mountpoint does not exist"); break;
case(ErrorCode::DockerPullRateLimit): errorMessage = QObject::tr("Docker error: The pull rate limit has been reached"); break;
case(ErrorCode::ServerLinuxKernelTooOld): errorMessage = QObject::tr("Server error: Linux kernel is too old"); break;
// Libssh errors
case(ErrorCode::SshRequestDeniedError): errorMessage = QObject::tr("SSH request was denied"); break;
+35 -61
View File
@@ -7,7 +7,6 @@ IpcClient::IpcClient(QObject *parent) : QObject(parent)
{
m_node.connectToNode(QUrl("local:" + amnezia::getIpcServiceUrl()));
m_interface.reset(m_node.acquire<IpcInterfaceReplica>());
m_tun2socks.reset(m_node.acquire<IpcProcessTun2SocksReplica>());
}
IpcClient& IpcClient::Instance()
@@ -33,68 +32,43 @@ QSharedPointer<IpcInterfaceReplica> IpcClient::Interface()
return rep;
}
QSharedPointer<IpcProcessTun2SocksReplica> IpcClient::InterfaceTun2Socks()
QSharedPointer<IpcProcessInterfaceReplica> IpcClient::CreatePrivilegedProcess()
{
QSharedPointer<IpcProcessTun2SocksReplica> rep = Instance().m_tun2socks;
if (rep.isNull()) {
qCritical() << "IpcClient::InterfaceTun2Socks: Replica is undefined";
return nullptr;
}
if (!rep->waitForSource(1000)) {
qCritical() << "IpcClient::InterfaceTun2Socks: Failed to initialize replica";
return nullptr;
}
if (!rep->isReplicaValid()) {
qWarning() << "IpcClient::InterfaceTun2Socks(): Replica is invalid";
}
return rep;
}
QSharedPointer<PrivilegedProcess> IpcClient::CreatePrivilegedProcess()
{
QSharedPointer<IpcInterfaceReplica> rep = Interface();
if (!rep) {
qCritical() << "IpcClient::createPrivilegedProcess: Replica is invalid";
return nullptr;
}
QRemoteObjectPendingReply<int> pidReply = rep->createPrivilegedProcess();
if (!pidReply.waitForFinished(5000)){
qCritical() << "IpcClient::createPrivilegedProcess: Failed to execute RO createPrivilegedProcess call";
return nullptr;
}
int pid = pidReply.returnValue();
QSharedPointer<ProcessDescriptor> pd(new ProcessDescriptor());
pd->localSocket.reset(new QLocalSocket(pd->replicaNode.data()));
connect(pd->localSocket.data(), &QLocalSocket::connected, pd->replicaNode.data(), [pd]() {
pd->replicaNode->addClientSideConnection(pd->localSocket.data());
IpcProcessInterfaceReplica *repl = pd->replicaNode->acquire<IpcProcessInterfaceReplica>();
// TODO: rework the unsafe cast below
PrivilegedProcess *priv = static_cast<PrivilegedProcess *>(repl);
pd->ipcProcess.reset(priv);
if (!pd->ipcProcess) {
qWarning() << "Acquire PrivilegedProcess failed";
} else {
pd->ipcProcess->waitForSource(1000);
if (!pd->ipcProcess->isReplicaValid()) {
qWarning() << "PrivilegedProcess replica is not connected!";
}
QObject::connect(pd->ipcProcess.data(), &PrivilegedProcess::destroyed, pd->ipcProcess.data(),
[pd]() { pd->replicaNode->deleteLater(); });
return withInterface([](QSharedPointer<IpcInterfaceReplica> &iface) -> QSharedPointer<IpcProcessInterfaceReplica> {
auto createPrivilegedProcess = iface->createPrivilegedProcess();
if (!createPrivilegedProcess.waitForFinished()) {
qCritical() << "Failed to create privileged process";
return nullptr;
}
});
pd->localSocket->connectToServer(amnezia::getIpcProcessUrl(pid));
if (!pd->localSocket->waitForConnected()) {
qCritical() << "IpcClient::createPrivilegedProcess: Failed to connect to process' socket";
const int pid = createPrivilegedProcess.returnValue();
auto* node = new QRemoteObjectNode();
node->connectToNode(QUrl(QString("local:%1").arg(amnezia::getIpcProcessUrl(pid))));
QSharedPointer<IpcProcessInterfaceReplica> rep(
node->acquire<IpcProcessInterfaceReplica>(),
[node] (IpcProcessInterfaceReplica *ptr) {
delete ptr;
node->deleteLater();
}
);
if (rep.isNull()) {
qCritical() << "IpcClient::CreatePrivilegedProcess(): Failed to acquire replica";
return nullptr;
}
if (!rep->waitForSource()) {
qCritical() << "IpcClient::CreatePrivilegedProcess(): Failed to initialize replica";
return nullptr;
}
if (!rep->isReplicaValid()) {
qCritical() << "IpcClient::CreatePrivilegedProcess(): Replica is invalid";
return nullptr;
}
return rep;
},
[]() -> QSharedPointer<IpcProcessInterfaceReplica> {
return nullptr;
}
auto processReplica = QSharedPointer<PrivilegedProcess>(pd->ipcProcess);
return processReplica;
});
}
+2 -17
View File
@@ -5,9 +5,7 @@
#include <QObject>
#include "rep_ipc_interface_replica.h"
#include "rep_ipc_process_tun2socks_replica.h"
#include "privileged_process.h"
#include "rep_ipc_process_interface_replica.h"
class IpcClient : public QObject
{
@@ -18,8 +16,7 @@ public:
static IpcClient& Instance();
static QSharedPointer<IpcInterfaceReplica> Interface();
static QSharedPointer<IpcProcessTun2SocksReplica> InterfaceTun2Socks();
static QSharedPointer<PrivilegedProcess> CreatePrivilegedProcess();
static QSharedPointer<IpcProcessInterfaceReplica> CreatePrivilegedProcess();
template <typename Func>
static auto withInterface(Func func)
@@ -54,18 +51,6 @@ signals:
private:
QRemoteObjectNode m_node;
QSharedPointer<IpcInterfaceReplica> m_interface;
QSharedPointer<IpcProcessTun2SocksReplica> m_tun2socks;
struct ProcessDescriptor {
ProcessDescriptor () {
replicaNode = QSharedPointer<QRemoteObjectNode>(new QRemoteObjectNode());
ipcProcess = QSharedPointer<PrivilegedProcess>();
localSocket = QSharedPointer<QLocalSocket>();
}
QSharedPointer<PrivilegedProcess> ipcProcess;
QSharedPointer<QRemoteObjectNode> replicaNode;
QSharedPointer<QLocalSocket> localSocket;
};
};
#endif // IPCCLIENT_H
+2 -3
View File
@@ -24,7 +24,7 @@
#include <sys/socket.h>
#include <unistd.h>
#endif
#if defined(Q_OS_MAC) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
#if defined(Q_OS_MAC) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !defined(MACOS_NE)
#include <sys/param.h>
#include <sys/sysctl.h>
#include <sys/socket.h>
@@ -44,7 +44,6 @@
#include <QHostAddress>
#include <QHostInfo>
#include <QDebug>
QRegularExpression NetworkUtilities::ipAddressRegExp()
{
@@ -405,7 +404,7 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
close(sock);
return { gateway_address, QNetworkInterface::interfaceFromName(interface) };
#endif
#if defined(Q_OS_MAC) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
#if defined(Q_OS_MAC) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !defined(MACOS_NE)
QString gateway;
int index = -1;
-27
View File
@@ -1,27 +0,0 @@
#include "privileged_process.h"
PrivilegedProcess::PrivilegedProcess() :
IpcProcessInterfaceReplica()
{
}
PrivilegedProcess::~PrivilegedProcess()
{
qDebug() << "PrivilegedProcess::~PrivilegedProcess()";
}
void PrivilegedProcess::waitForFinished(int msecs)
{
QSharedPointer<QEventLoop> loop(new QEventLoop);
connect(this, &PrivilegedProcess::finished, this, [this, loop](int exitCode, QProcess::ExitStatus exitStatus) mutable{
loop->quit();
loop.clear();
});
QTimer::singleShot(msecs, this, [this, loop]() mutable {
loop->quit();
loop.clear();
});
loop->exec();
}
-24
View File
@@ -1,24 +0,0 @@
#ifndef PRIVILEGED_PROCESS_H
#define PRIVILEGED_PROCESS_H
#include <QObject>
#include "rep_ipc_process_interface_replica.h"
// This class is dangerous - instance of this class casted from base class,
// so it support only functions
// Do not add any members into it
//
class PrivilegedProcess : public IpcProcessInterfaceReplica
{
Q_OBJECT
public:
PrivilegedProcess();
~PrivilegedProcess() override;
void waitForFinished(int msecs);
};
#endif // PRIVILEGED_PROCESS_H
-153
View File
@@ -1,153 +0,0 @@
#include "dnsPacket_p.h"
#include <QHostInfo>
#include <cstring>
namespace amnezia::transport::dns::detail
{
QHostAddress resolveHostAddress(const QString &host)
{
QHostAddress addr(host);
if (!addr.isNull()) return addr;
QHostInfo info = QHostInfo::fromName(host);
if (!info.addresses().isEmpty()) return info.addresses().first();
return QHostAddress();
}
QByteArray encodeDnsName(const QString &hostname)
{
QByteArray result;
const QStringList parts = hostname.split('.');
for (const QString &part : parts) {
if (part.length() > 63) {
return QByteArray();
}
result.append(static_cast<char>(part.length()));
result.append(part.toUtf8());
}
result.append(static_cast<char>(0));
return result;
}
QByteArray buildDnsQuery(const QString &hostname, quint16 transactionId)
{
QByteArray packet;
DnsHeader header;
header.id = qToBigEndian(transactionId);
header.flags = qToBigEndian<quint16>(0x0100);
header.qdcount = qToBigEndian<quint16>(1);
header.ancount = 0;
header.nscount = 0;
header.arcount = 0;
packet.append(reinterpret_cast<const char *>(&header), sizeof(DnsHeader));
const QByteArray qname = encodeDnsName(hostname);
if (qname.isEmpty()) {
return QByteArray();
}
packet.append(qname);
quint16 qtype = qToBigEndian<quint16>(DNS_TYPE_A);
packet.append(reinterpret_cast<const char *>(&qtype), sizeof(quint16));
quint16 qclass = qToBigEndian<quint16>(DNS_CLASS_IN);
packet.append(reinterpret_cast<const char *>(&qclass), sizeof(quint16));
return packet;
}
QString parseDnsResponse(const QByteArray &response, bool isTcp)
{
if (response.size() < static_cast<int>(sizeof(DnsHeader))) {
return QString();
}
int offset = isTcp ? 2 : 0;
if (response.size() < offset + static_cast<int>(sizeof(DnsHeader))) {
return QString();
}
DnsHeader header;
std::memcpy(&header, response.constData() + offset, sizeof(DnsHeader));
offset += sizeof(DnsHeader);
const quint16 flags = qFromBigEndian(header.flags);
const quint16 ancount = qFromBigEndian(header.ancount);
if ((flags & 0x8000) == 0 || (flags & 0x000F) != 0) {
return QString();
}
if (ancount == 0) {
return QString();
}
while (offset < response.size() && response.at(offset) != 0) {
const quint8 length = static_cast<quint8>(response.at(offset));
if (length > 63) {
return QString();
}
offset += length + 1;
}
if (offset >= response.size()) {
return QString();
}
offset++;
offset += 4;
for (int i = 0; i < ancount && offset < response.size(); ++i) {
if (offset >= response.size()) {
break;
}
const quint8 nameByte = static_cast<quint8>(response.at(offset));
if ((nameByte & 0xC0) == 0xC0) {
offset += 2;
} else {
while (offset < response.size() && response.at(offset) != 0) {
const quint8 length = static_cast<quint8>(response.at(offset));
if (length > 63) {
return QString();
}
offset += length + 1;
}
offset++;
}
if (offset + 10 > response.size()) {
break;
}
const quint16 type =
qFromBigEndian<quint16>(*reinterpret_cast<const quint16 *>(response.constData() + offset));
offset += 2;
offset += 2;
offset += 4;
const quint16 rdlength =
qFromBigEndian<quint16>(*reinterpret_cast<const quint16 *>(response.constData() + offset));
offset += 2;
if (type == DNS_TYPE_A && rdlength == 4) {
if (offset + 4 > response.size()) {
break;
}
QHostAddress ip;
ip.setAddress(
qFromBigEndian<quint32>(*reinterpret_cast<const quint32 *>(response.constData() + offset)));
return ip.toString();
}
offset += rdlength;
}
return QString();
}
} // namespace amnezia::transport::dns::detail
-38
View File
@@ -1,38 +0,0 @@
#ifndef DNSPACKET_P_H
#define DNSPACKET_P_H
#include <QByteArray>
#include <QHostAddress>
#include <QString>
#include <QtEndian>
namespace amnezia::transport::dns::detail
{
constexpr quint16 DNS_PORT = 53;
constexpr quint16 DNS_TYPE_A = 1;
constexpr quint16 DNS_CLASS_IN = 1;
#pragma pack(push, 1)
struct DnsHeader
{
quint16 id;
quint16 flags;
quint16 qdcount;
quint16 ancount;
quint16 nscount;
quint16 arcount;
};
#pragma pack(pop)
QHostAddress resolveHostAddress(const QString &host);
QByteArray encodeDnsName(const QString &hostname);
QByteArray buildDnsQuery(const QString &hostname, quint16 transactionId);
QString parseDnsResponse(const QByteArray &response, bool isTcp);
} // namespace amnezia::transport::dns::detail
#endif // DNSPACKET_P_H
-354
View File
@@ -1,354 +0,0 @@
#include "dnsResolver.h"
#include "dnsPacket_p.h"
#include <QDateTime>
#include <QEventLoop>
#include <QHostAddress>
#include <QNetworkAccessManager>
#include <QNetworkDatagram>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QSslSocket>
#include <QTcpSocket>
#include <QTimer>
#include <QUdpSocket>
#include <QUrl>
namespace amnezia::transport::dns::DnsResolver
{
using detail::buildDnsQuery;
using detail::parseDnsResponse;
using detail::resolveHostAddress;
QString resolve(const QString &hostname,
const QString &dnsServer,
DnsProtocol protocol,
quint16 port,
int timeoutMsecs,
const QString &dohEndpoint)
{
switch (protocol) {
case DnsProtocol::Udp:
return resolveOverUdp(hostname, dnsServer, port, timeoutMsecs);
case DnsProtocol::Tcp:
return resolveOverTcp(hostname, dnsServer, port, timeoutMsecs);
case DnsProtocol::Tls:
return resolveOverTls(hostname, dnsServer, port, timeoutMsecs);
case DnsProtocol::Https:
return resolveOverHttps(hostname, dnsServer, dohEndpoint, timeoutMsecs);
case DnsProtocol::Quic:
return resolveOverQuic(hostname, dnsServer, port, timeoutMsecs);
}
return QString();
}
QString resolveOverUdp(const QString &hostname, const QString &dnsServer, quint16 port, int timeoutMsecs)
{
QUdpSocket socket;
const quint16 transactionId = static_cast<quint16>(QDateTime::currentMSecsSinceEpoch() & 0xFFFF);
const QByteArray query = buildDnsQuery(hostname, transactionId);
if (query.isEmpty()) {
return QString();
}
const QHostAddress dnsAddress = resolveHostAddress(dnsServer);
if (dnsAddress.isNull()) {
return QString();
}
const qint64 bytesWritten = socket.writeDatagram(query, dnsAddress, port);
if (bytesWritten != query.size()) {
return QString();
}
QEventLoop loop;
QTimer timer;
timer.setSingleShot(true);
timer.setInterval(timeoutMsecs);
QByteArray response;
bool responseReceived = false;
QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
QObject::connect(&socket, &QUdpSocket::readyRead, [&]() {
while (socket.hasPendingDatagrams()) {
QNetworkDatagram datagram = socket.receiveDatagram();
if (datagram.isValid()) {
response = datagram.data();
responseReceived = true;
loop.quit();
}
}
});
timer.start();
loop.exec();
timer.stop();
if (!responseReceived || response.isEmpty()) {
return QString();
}
return parseDnsResponse(response, false);
}
QString resolveOverTcp(const QString &hostname, const QString &dnsServer, quint16 port, int timeoutMsecs)
{
QTcpSocket socket;
const QHostAddress dnsAddress = resolveHostAddress(dnsServer);
if (dnsAddress.isNull()) {
return QString();
}
socket.connectToHost(dnsAddress, port);
if (!socket.waitForConnected(timeoutMsecs)) {
return QString();
}
const quint16 transactionId = static_cast<quint16>(QDateTime::currentMSecsSinceEpoch() & 0xFFFF);
const QByteArray query = buildDnsQuery(hostname, transactionId);
if (query.isEmpty()) {
socket.close();
return QString();
}
quint16 length = qToBigEndian<quint16>(static_cast<quint16>(query.size()));
QByteArray tcpQuery;
tcpQuery.append(reinterpret_cast<const char *>(&length), sizeof(quint16));
tcpQuery.append(query);
const qint64 bytesWritten = socket.write(tcpQuery);
if (bytesWritten != tcpQuery.size() || !socket.waitForBytesWritten(timeoutMsecs)) {
socket.close();
return QString();
}
QEventLoop loop;
QTimer timer;
timer.setSingleShot(true);
timer.setInterval(timeoutMsecs);
QByteArray response;
bool responseReceived = false;
QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
QObject::connect(&socket, &QTcpSocket::readyRead, [&]() {
if (socket.bytesAvailable() >= 2 && response.isEmpty()) {
QByteArray lengthBytes = socket.read(2);
if (lengthBytes.size() == 2) {
const quint16 responseLength =
qFromBigEndian<quint16>(*reinterpret_cast<const quint16 *>(lengthBytes.constData()));
while (socket.bytesAvailable() < responseLength) {
if (!socket.waitForReadyRead(timeoutMsecs / 2)) {
break;
}
}
if (socket.bytesAvailable() >= responseLength) {
response = socket.read(responseLength);
responseReceived = true;
loop.quit();
}
}
}
});
timer.start();
loop.exec();
timer.stop();
socket.close();
if (!responseReceived || response.isEmpty()) {
return QString();
}
return parseDnsResponse(response, true);
}
QString resolveOverTls(const QString &hostname, const QString &dnsServer, quint16 port, int timeoutMsecs)
{
QSslSocket socket;
const QHostAddress dnsAddress = resolveHostAddress(dnsServer);
if (dnsAddress.isNull()) {
return QString();
}
socket.setPeerVerifyMode(QSslSocket::QueryPeer);
socket.connectToHostEncrypted(dnsAddress.toString(), port);
if (!socket.waitForConnected(timeoutMsecs)) {
return QString();
}
if (!socket.waitForEncrypted(timeoutMsecs)) {
socket.close();
return QString();
}
const quint16 transactionId = static_cast<quint16>(QDateTime::currentMSecsSinceEpoch() & 0xFFFF);
const QByteArray query = buildDnsQuery(hostname, transactionId);
if (query.isEmpty()) {
socket.close();
return QString();
}
quint16 length = qToBigEndian<quint16>(static_cast<quint16>(query.size()));
QByteArray tlsQuery;
tlsQuery.append(reinterpret_cast<const char *>(&length), sizeof(quint16));
tlsQuery.append(query);
const qint64 bytesWritten = socket.write(tlsQuery);
if (bytesWritten != tlsQuery.size() || !socket.waitForBytesWritten(timeoutMsecs)) {
socket.close();
return QString();
}
QEventLoop loop;
QTimer timer;
timer.setSingleShot(true);
timer.setInterval(timeoutMsecs);
QByteArray response;
bool responseReceived = false;
QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
QObject::connect(&socket, &QSslSocket::readyRead, [&]() {
if (socket.bytesAvailable() >= 2 && response.isEmpty()) {
QByteArray lengthBytes = socket.read(2);
if (lengthBytes.size() == 2) {
const quint16 responseLength =
qFromBigEndian<quint16>(*reinterpret_cast<const quint16 *>(lengthBytes.constData()));
while (socket.bytesAvailable() < responseLength) {
if (!socket.waitForReadyRead(timeoutMsecs / 2)) {
break;
}
}
if (socket.bytesAvailable() >= responseLength) {
response = socket.read(responseLength);
responseReceived = true;
loop.quit();
}
}
}
});
timer.start();
loop.exec();
timer.stop();
socket.close();
if (!responseReceived || response.isEmpty()) {
return QString();
}
return parseDnsResponse(response, true);
}
QString resolveOverHttps(const QString &hostname, const QString &dnsServer, const QString &endpoint, int timeoutMsecs)
{
const QString dohUrl = QStringLiteral("https://%1%2").arg(dnsServer, endpoint);
const quint16 transactionId = static_cast<quint16>(QDateTime::currentMSecsSinceEpoch() & 0xFFFF);
const QByteArray query = buildDnsQuery(hostname, transactionId);
if (query.isEmpty()) {
return QString();
}
QNetworkRequest request;
request.setUrl(QUrl(dohUrl));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/dns-message");
request.setRawHeader("Accept", "application/dns-message");
request.setTransferTimeout(timeoutMsecs);
QNetworkAccessManager nam;
QNetworkReply *reply = nam.post(request, query);
QEventLoop loop;
QTimer timer;
timer.setSingleShot(true);
timer.setInterval(timeoutMsecs);
QByteArray response;
bool responseReceived = false;
QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
QObject::connect(reply, &QNetworkReply::finished, [&]() {
if (reply->error() == QNetworkReply::NoError) {
response = reply->readAll();
responseReceived = true;
}
loop.quit();
});
timer.start();
loop.exec();
timer.stop();
reply->deleteLater();
if (!responseReceived || response.isEmpty()) {
return QString();
}
return parseDnsResponse(response, false);
}
QString resolveOverQuic(const QString &hostname, const QString &dnsServer, quint16 port, int timeoutMsecs)
{
// QUIC требует специальной библиотеки — пока используем UDP fallback
QUdpSocket socket;
const QHostAddress dnsAddress = resolveHostAddress(dnsServer);
if (dnsAddress.isNull()) {
return QString();
}
const quint16 transactionId = static_cast<quint16>(QDateTime::currentMSecsSinceEpoch() & 0xFFFF);
const QByteArray query = buildDnsQuery(hostname, transactionId);
if (query.isEmpty()) {
return QString();
}
const qint64 bytesWritten = socket.writeDatagram(query, dnsAddress, port);
if (bytesWritten != query.size()) {
return QString();
}
QEventLoop loop;
QTimer timer;
timer.setSingleShot(true);
timer.setInterval(timeoutMsecs);
QByteArray response;
bool responseReceived = false;
QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
QObject::connect(&socket, &QUdpSocket::readyRead, [&]() {
while (socket.hasPendingDatagrams()) {
QNetworkDatagram datagram = socket.receiveDatagram();
if (datagram.isValid()) {
response = datagram.data();
responseReceived = true;
loop.quit();
}
}
});
timer.start();
loop.exec();
timer.stop();
if (!responseReceived || response.isEmpty()) {
return QString();
}
return parseDnsResponse(response, false);
}
} // namespace amnezia::transport::dns::DnsResolver
-29
View File
@@ -1,29 +0,0 @@
#ifndef DNSRESOLVER_H
#define DNSRESOLVER_H
#include <QString>
namespace amnezia::transport::dns
{
enum class DnsProtocol { Udp, Tcp, Tls, Https, Quic };
namespace DnsResolver
{
QString resolve(const QString &hostname,
const QString &dnsServer,
DnsProtocol protocol,
quint16 port,
int timeoutMsecs = 3000,
const QString &dohEndpoint = QStringLiteral("/dns-query"));
QString resolveOverUdp(const QString &hostname, const QString &dnsServer, quint16 port, int timeoutMsecs = 3000);
QString resolveOverTcp(const QString &hostname, const QString &dnsServer, quint16 port, int timeoutMsecs = 3000);
QString resolveOverTls(const QString &hostname, const QString &dnsServer, quint16 port, int timeoutMsecs = 3000);
QString resolveOverHttps(const QString &hostname, const QString &dnsServer, const QString &endpoint, int timeoutMsecs = 3000);
QString resolveOverQuic(const QString &hostname, const QString &dnsServer, quint16 port, int timeoutMsecs = 3000);
} // namespace DnsResolver
} // namespace amnezia::transport::dns
#endif // DNSRESOLVER_H
-817
View File
@@ -1,817 +0,0 @@
#include "dnsTunnel.h"
#include "dnsPacket_p.h"
#include <QDateTime>
#include <QDebug>
#include <QElapsedTimer>
#include <QEventLoop>
#include <QHostAddress>
#include <QList>
#include <QMap>
#include <QNetworkAccessManager>
#include <QNetworkDatagram>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QSharedPointer>
#include <QSslError>
#include <QSslSocket>
#include <QStringList>
#include <QTcpSocket>
#include <QThread>
#include <QTimer>
#include <QUdpSocket>
#include <QUrl>
namespace amnezia::transport::dns::DnsTunnel
{
using detail::resolveHostAddress;
namespace
{
constexpr quint16 EDNS0_PAYLOAD_OPTION_CODE = 65001;
constexpr quint16 EDNS0_CHUNK_REQUEST_CODE = 65002;
constexpr quint16 EDNS0_CHUNK_RESPONSE_CODE = 65003;
struct ChunkMeta
{
QByteArray chunkId;
quint16 totalChunks = 0;
quint16 chunkIndex = 0;
quint32 totalSize = 0;
};
void appendUint16BE(QByteArray &data, quint16 value)
{
data.append(static_cast<char>((value >> 8) & 0xFF));
data.append(static_cast<char>(value & 0xFF));
}
QByteArray buildDnsChunkRequest(const QString &queryName, quint16 transactionId,
const QByteArray &chunkId, quint16 chunkIndex)
{
QByteArray query;
appendUint16BE(query, transactionId);
appendUint16BE(query, 0x0100);
appendUint16BE(query, 1);
appendUint16BE(query, 0);
appendUint16BE(query, 0);
appendUint16BE(query, 1);
const QStringList labels = queryName.split('.');
for (const QString &label : labels) {
QByteArray labelBytes = label.toUtf8();
query.append(static_cast<char>(labelBytes.size()));
query.append(labelBytes);
}
query.append(static_cast<char>(0));
appendUint16BE(query, 16);
appendUint16BE(query, 1);
const quint16 optionDataLen = 4 + 18;
query.append(static_cast<char>(0));
appendUint16BE(query, 41);
appendUint16BE(query, 4096);
query.append(static_cast<char>(0));
query.append(static_cast<char>(0));
appendUint16BE(query, 0);
appendUint16BE(query, optionDataLen);
appendUint16BE(query, EDNS0_CHUNK_REQUEST_CODE);
appendUint16BE(query, 18);
query.append(chunkId.left(16).leftJustified(16, '\0'));
appendUint16BE(query, chunkIndex);
return query;
}
ChunkMeta parseChunkMeta(const QByteArray &response)
{
ChunkMeta meta;
if (response.size() < 12) return meta;
const quint8 *data = reinterpret_cast<const quint8 *>(response.constData());
const quint16 qdCount = (data[4] << 8) | data[5];
const quint16 anCount = (data[6] << 8) | data[7];
const quint16 nsCount = (data[8] << 8) | data[9];
const quint16 arCount = (data[10] << 8) | data[11];
int pos = 12;
auto skipDnsName = [&]() -> bool {
int maxLabels = 128;
while (pos < response.size() && data[pos] != 0 && maxLabels-- > 0) {
if ((data[pos] & 0xC0) == 0xC0) {
pos += 2;
return pos <= response.size();
}
const int labelLen = data[pos];
if (pos + 1 + labelLen > response.size()) return false;
pos += labelLen + 1;
}
if (pos < response.size() && data[pos] == 0) pos++;
return pos <= response.size();
};
for (int i = 0; i < qdCount && pos < response.size(); ++i) {
if (!skipDnsName()) return meta;
if (pos + 4 > response.size()) return meta;
pos += 4;
}
for (int i = 0; i < anCount && pos < response.size(); ++i) {
if (!skipDnsName()) return meta;
if (pos + 10 > response.size()) return meta;
const quint16 rdlen = (data[pos + 8] << 8) | data[pos + 9];
if (pos + 10 + rdlen > response.size()) return meta;
pos += 10 + rdlen;
}
for (int i = 0; i < nsCount && pos < response.size(); ++i) {
if (!skipDnsName()) return meta;
if (pos + 10 > response.size()) return meta;
const quint16 rdlen = (data[pos + 8] << 8) | data[pos + 9];
if (pos + 10 + rdlen > response.size()) return meta;
pos += 10 + rdlen;
}
for (int i = 0; i < arCount && pos < response.size(); ++i) {
if (pos < response.size() && data[pos] == 0) {
pos++;
} else {
if (!skipDnsName()) return meta;
}
if (pos + 10 > response.size()) return meta;
const quint16 rtype = (data[pos] << 8) | data[pos + 1];
const quint16 rdlen = (data[pos + 8] << 8) | data[pos + 9];
if (pos + 10 + rdlen > response.size()) return meta;
pos += 10;
if (rtype == 41 && rdlen > 0) {
const int optEnd = pos + rdlen;
while (pos + 4 <= optEnd) {
const quint16 optCode = (data[pos] << 8) | data[pos + 1];
const quint16 optLen = (data[pos + 2] << 8) | data[pos + 3];
pos += 4;
if (optCode == EDNS0_CHUNK_RESPONSE_CODE && optLen >= 24) {
meta.chunkId = QByteArray(reinterpret_cast<const char *>(data + pos), 16);
meta.totalChunks = (data[pos + 16] << 8) | data[pos + 17];
meta.chunkIndex = (data[pos + 18] << 8) | data[pos + 19];
meta.totalSize = (static_cast<quint32>(data[pos + 20]) << 24)
| (static_cast<quint32>(data[pos + 21]) << 16)
| (static_cast<quint32>(data[pos + 22]) << 8) | data[pos + 23];
return meta;
}
pos += optLen;
}
} else {
pos += rdlen;
}
}
return meta;
}
QByteArray buildDnsTxtQueryWithPayload(const QString &queryName, quint16 transactionId, const QByteArray &payload)
{
QByteArray query;
appendUint16BE(query, transactionId);
appendUint16BE(query, 0x0100);
appendUint16BE(query, 1);
appendUint16BE(query, 0);
appendUint16BE(query, 0);
appendUint16BE(query, 1);
const QStringList labels = queryName.split('.');
for (const QString &label : labels) {
QByteArray labelBytes = label.toUtf8();
query.append(static_cast<char>(labelBytes.size()));
query.append(labelBytes);
}
query.append(static_cast<char>(0));
appendUint16BE(query, 16);
appendUint16BE(query, 1);
const QByteArray payloadBase64 = payload.toBase64();
const quint16 optionDataLen = 4 + payloadBase64.size();
query.append(static_cast<char>(0));
appendUint16BE(query, 41);
appendUint16BE(query, 4096);
query.append(static_cast<char>(0));
query.append(static_cast<char>(0));
appendUint16BE(query, 0);
appendUint16BE(query, optionDataLen);
appendUint16BE(query, EDNS0_PAYLOAD_OPTION_CODE);
appendUint16BE(query, payloadBase64.size());
query.append(payloadBase64);
return query;
}
QByteArray parseDnsTxtResponse(const QByteArray &response)
{
if (response.size() < 12) {
return QByteArray();
}
const uchar *data = reinterpret_cast<const uchar *>(response.constData());
int pos = 0;
pos += 2;
const quint16 flags = (data[pos] << 8) | data[pos + 1]; pos += 2;
const quint16 qdCount = (data[pos] << 8) | data[pos + 1]; pos += 2;
const quint16 anCount = (data[pos] << 8) | data[pos + 1]; pos += 2;
pos += 2;
pos += 2;
if ((flags & 0x8000) == 0) {
return QByteArray();
}
if (anCount > 100 || qdCount > 10) {
return QByteArray();
}
auto skipDnsName = [&]() -> bool {
int maxLabels = 128;
while (pos < response.size() && data[pos] != 0 && maxLabels-- > 0) {
if ((data[pos] & 0xC0) == 0xC0) {
pos += 2;
return pos <= response.size();
}
const int labelLen = data[pos];
if (pos + 1 + labelLen > response.size()) return false;
pos += labelLen + 1;
}
if (pos < response.size() && data[pos] == 0) pos++;
return pos <= response.size();
};
for (int i = 0; i < qdCount && pos < response.size(); ++i) {
if (!skipDnsName()) {
return QByteArray();
}
if (pos + 4 > response.size()) return QByteArray();
pos += 4;
}
QByteArray combinedTxt;
for (int i = 0; i < anCount && pos < response.size(); ++i) {
if (!skipDnsName()) {
break;
}
if (pos + 10 > response.size()) {
break;
}
const quint16 rtype = (data[pos] << 8) | data[pos + 1]; pos += 2;
pos += 2; // class
pos += 4; // ttl
const quint16 rdlength = (data[pos] << 8) | data[pos + 1]; pos += 2;
if (pos + rdlength > response.size()) {
break;
}
if (rtype == 16) {
const int rdEnd = pos + rdlength;
while (pos < rdEnd && pos < response.size()) {
const quint8 txtLen = data[pos++];
if (txtLen > 0 && pos + txtLen <= rdEnd && pos + txtLen <= response.size()) {
combinedTxt.append(reinterpret_cast<const char *>(data + pos), txtLen);
pos += txtLen;
} else {
break;
}
}
} else {
pos += rdlength;
}
}
if (combinedTxt.isEmpty()) {
return QByteArray();
}
return QByteArray::fromBase64(combinedTxt);
}
} // namespace
QByteArray send(const QByteArray &payload,
const QString &endpointName,
const QString &baseDomain,
const QString &dnsServer,
DnsProtocol protocol,
quint16 port,
int timeoutMsecs,
const QString &dohEndpoint)
{
const QString queryName = QStringLiteral("%1.%2").arg(endpointName, baseDomain);
switch (protocol) {
case DnsProtocol::Udp:
return sendOverUdpChunked(payload, queryName, dnsServer, port, timeoutMsecs);
case DnsProtocol::Tcp:
return sendOverTcp(payload, queryName, dnsServer, port, timeoutMsecs);
case DnsProtocol::Tls:
return sendOverTls(payload, queryName, dnsServer, port, timeoutMsecs);
case DnsProtocol::Https:
return sendOverHttps(payload, queryName, dnsServer, port, dohEndpoint, timeoutMsecs);
case DnsProtocol::Quic:
return QByteArray();
}
return QByteArray();
}
QByteArray sendOverUdp(const QByteArray &payload, const QString &queryName,
const QString &dnsServer, quint16 port, int timeoutMsecs)
{
QUdpSocket socket;
const quint16 transactionId = static_cast<quint16>(QDateTime::currentMSecsSinceEpoch() & 0xFFFF);
const QByteArray query = buildDnsTxtQueryWithPayload(queryName, transactionId, payload);
if (query.isEmpty()) {
return QByteArray();
}
const QHostAddress dnsAddress = resolveHostAddress(dnsServer);
if (dnsAddress.isNull()) {
return QByteArray();
}
const qint64 bytesWritten = socket.writeDatagram(query, dnsAddress, port);
if (bytesWritten != query.size()) {
return QByteArray();
}
QElapsedTimer timer;
timer.start();
while (timer.elapsed() < timeoutMsecs) {
if (socket.waitForReadyRead(qMax(1, timeoutMsecs - static_cast<int>(timer.elapsed())))) {
while (socket.hasPendingDatagrams()) {
QNetworkDatagram datagram = socket.receiveDatagram();
if (datagram.isValid()) {
return parseDnsTxtResponse(datagram.data());
}
}
}
}
return QByteArray();
}
QByteArray sendOverTcp(const QByteArray &payload, const QString &queryName,
const QString &dnsServer, quint16 port, int timeoutMsecs)
{
qDebug() << "[DNS-TCP] start: queryName=" << queryName << "server=" << dnsServer
<< "port=" << port << "payloadBytes=" << payload.size();
QTcpSocket socket;
const QHostAddress dnsAddress = resolveHostAddress(dnsServer);
if (dnsAddress.isNull()) {
qWarning() << "[DNS-TCP] failed to resolve" << dnsServer;
return QByteArray();
}
socket.connectToHost(dnsAddress, port);
if (!socket.waitForConnected(timeoutMsecs)) {
qWarning() << "[DNS-TCP] connect failed:" << socket.errorString();
return QByteArray();
}
qDebug() << "[DNS-TCP] connected";
const quint16 transactionId = static_cast<quint16>(QDateTime::currentMSecsSinceEpoch() & 0xFFFF);
const QByteArray query = buildDnsTxtQueryWithPayload(queryName, transactionId, payload);
if (query.isEmpty()) {
qWarning() << "[DNS-TCP] failed to build DNS query";
socket.close();
return QByteArray();
}
qDebug() << "[DNS-TCP] built DNS query bytes=" << query.size() << "txid=" << transactionId;
quint16 length = qToBigEndian<quint16>(static_cast<quint16>(query.size()));
QByteArray tcpQuery;
tcpQuery.append(reinterpret_cast<const char *>(&length), sizeof(quint16));
tcpQuery.append(query);
const qint64 bytesWritten = socket.write(tcpQuery);
qDebug() << "[DNS-TCP] wrote bytes=" << bytesWritten << "/ expected=" << tcpQuery.size();
if (bytesWritten != tcpQuery.size() || !socket.waitForBytesWritten(timeoutMsecs)) {
qWarning() << "[DNS-TCP] write failed:" << socket.errorString();
socket.close();
return QByteArray();
}
QElapsedTimer timer;
timer.start();
while (socket.bytesAvailable() < 2) {
const int remaining = timeoutMsecs - timer.elapsed();
if (remaining <= 0 || !socket.waitForReadyRead(remaining)) {
qWarning() << "[DNS-TCP] timeout waiting for response length, socketState="
<< socket.state() << "err=" << socket.errorString()
<< "bytesAvailable=" << socket.bytesAvailable();
socket.close();
return QByteArray();
}
}
QByteArray lengthBytes = socket.read(2);
if (lengthBytes.size() != 2) {
qWarning() << "[DNS-TCP] could not read length prefix";
socket.close();
return QByteArray();
}
const quint16 responseLength =
qFromBigEndian<quint16>(*reinterpret_cast<const quint16 *>(lengthBytes.constData()));
qDebug() << "[DNS-TCP] response length prefix=" << responseLength;
QByteArray response;
while (response.size() < responseLength) {
const int remaining = timeoutMsecs - timer.elapsed();
if (remaining <= 0) {
qWarning() << "[DNS-TCP] timeout reading body, got" << response.size() << "/" << responseLength;
socket.close();
return QByteArray();
}
if (socket.bytesAvailable() > 0) {
response.append(socket.read(responseLength - response.size()));
} else if (!socket.waitForReadyRead(remaining)) {
qWarning() << "[DNS-TCP] timeout in waitForReadyRead, got" << response.size() << "/" << responseLength;
socket.close();
return QByteArray();
}
}
qDebug() << "[DNS-TCP] full response read, bytes=" << response.size();
socket.close();
QByteArray parsed = parseDnsTxtResponse(response);
qDebug() << "[DNS-TCP] parsed TXT payload bytes=" << parsed.size();
return parsed;
}
QByteArray sendOverTls(const QByteArray &payload, const QString &queryName,
const QString &dnsServer, quint16 port, int timeoutMsecs)
{
QSslSocket socket;
#ifdef AGW_INSECURE_SSL
socket.setPeerVerifyMode(QSslSocket::VerifyNone);
QObject::connect(&socket, QOverload<const QList<QSslError> &>::of(&QSslSocket::sslErrors),
&socket, [&socket](const QList<QSslError> &errs) {
qWarning() << "[DoT] sslErrors (ignored, AGW_INSECURE_SSL=1):" << errs;
socket.ignoreSslErrors();
});
#else
socket.setPeerVerifyMode(QSslSocket::VerifyPeer);
#endif
const QHostAddress dnsAddress = resolveHostAddress(dnsServer);
if (dnsAddress.isNull()) {
qWarning() << "[DoT] failed to resolve" << dnsServer;
return QByteArray();
}
socket.connectToHostEncrypted(dnsServer, port);
if (!socket.waitForEncrypted(timeoutMsecs)) {
qWarning() << "[DoT] handshake failed:" << socket.errorString();
return QByteArray();
}
const quint16 transactionId = static_cast<quint16>(QDateTime::currentMSecsSinceEpoch() & 0xFFFF);
const QByteArray query = buildDnsTxtQueryWithPayload(queryName, transactionId, payload);
if (query.isEmpty()) {
socket.close();
return QByteArray();
}
quint16 length = qToBigEndian<quint16>(static_cast<quint16>(query.size()));
QByteArray tcpQuery;
tcpQuery.append(reinterpret_cast<const char *>(&length), sizeof(quint16));
tcpQuery.append(query);
const qint64 bytesWritten = socket.write(tcpQuery);
if (bytesWritten != tcpQuery.size() || !socket.waitForBytesWritten(timeoutMsecs)) {
socket.close();
return QByteArray();
}
QElapsedTimer timer;
timer.start();
while (socket.bytesAvailable() < 2) {
const int remaining = timeoutMsecs - timer.elapsed();
if (remaining <= 0 || !socket.waitForReadyRead(remaining)) {
socket.close();
return QByteArray();
}
}
QByteArray lengthBytes = socket.read(2);
if (lengthBytes.size() != 2) {
socket.close();
return QByteArray();
}
const quint16 responseLength =
qFromBigEndian<quint16>(*reinterpret_cast<const quint16 *>(lengthBytes.constData()));
QByteArray response;
while (response.size() < responseLength) {
const int remaining = timeoutMsecs - timer.elapsed();
if (remaining <= 0) {
socket.close();
return QByteArray();
}
if (socket.bytesAvailable() > 0) {
response.append(socket.read(responseLength - response.size()));
} else if (!socket.waitForReadyRead(remaining)) {
socket.close();
return QByteArray();
}
}
socket.close();
return parseDnsTxtResponse(response);
}
QByteArray sendOverHttps(const QByteArray &payload, const QString &queryName,
const QString &dnsServer, quint16 port, const QString &endpoint, int timeoutMsecs)
{
const quint16 transactionId = static_cast<quint16>(QDateTime::currentMSecsSinceEpoch() & 0xFFFF);
const QByteArray dnsQuery = buildDnsTxtQueryWithPayload(queryName, transactionId, payload);
qDebug() << "[DoH] queryName=" << queryName << "payloadBytes=" << payload.size()
<< "dnsQueryBytes=" << dnsQuery.size() << "txid=" << transactionId;
if (dnsQuery.isEmpty()) {
qWarning() << "[DoH] failed to build DNS query (payload too big or queryName invalid)";
return QByteArray();
}
const QString scheme = (port == 443) ? QStringLiteral("https") : QStringLiteral("http");
const QString url = QStringLiteral("%1://%2:%3%4").arg(scheme).arg(dnsServer).arg(port).arg(endpoint);
qDebug() << "[DoH] POST" << url << "timeoutMs=" << timeoutMsecs;
QNetworkRequest request((QUrl(url)));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/dns-message");
request.setRawHeader("Accept", "application/dns-message");
request.setTransferTimeout(timeoutMsecs);
QNetworkAccessManager manager;
QNetworkReply *reply = manager.post(request, dnsQuery);
QObject::connect(reply, &QNetworkReply::sslErrors, reply,
[reply](const QList<QSslError> &errs) {
qWarning() << "[DoH] sslErrors:" << errs;
#ifdef AGW_INSECURE_SSL
qWarning() << "[DoH] AGW_INSECURE_SSL=1, ignoring SSL errors";
reply->ignoreSslErrors();
#endif
});
QEventLoop loop;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
QTimer::singleShot(timeoutMsecs, &loop, &QEventLoop::quit);
loop.exec();
if (!reply->isFinished()) {
qWarning() << "[DoH] timeout after" << timeoutMsecs << "ms, aborting";
reply->abort();
reply->deleteLater();
return QByteArray();
}
if (reply->error() != QNetworkReply::NoError) {
qWarning() << "[DoH] reply error:" << reply->error() << reply->errorString()
<< "httpStatus=" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
reply->deleteLater();
return QByteArray();
}
QByteArray response = reply->readAll();
qDebug() << "[DoH] raw HTTP response bytes=" << response.size()
<< "httpStatus=" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
reply->deleteLater();
if (response.isEmpty()) {
qWarning() << "[DoH] empty HTTP response body";
return QByteArray();
}
QByteArray parsed = parseDnsTxtResponse(response);
qDebug() << "[DoH] parsed TXT payload bytes=" << parsed.size();
return parsed;
}
QByteArray sendOverUdpChunked(const QByteArray &payload, const QString &queryName,
const QString &dnsServer, quint16 port, int timeoutMsecs)
{
qDebug() << "[DNS-UDP] start: queryName=" << queryName << "server=" << dnsServer
<< "port=" << port << "payloadBytes=" << payload.size() << "timeoutMs=" << timeoutMsecs;
const QHostAddress dnsAddress = resolveHostAddress(dnsServer);
if (dnsAddress.isNull()) {
qWarning() << "[DNS-UDP] failed to resolve" << dnsServer;
return QByteArray();
}
qDebug() << "[DNS-UDP] resolved to" << dnsAddress.toString();
constexpr int MAX_INITIAL_RETRIES = 3;
constexpr int MAX_CHUNK_RETRIES = 2;
constexpr int MAX_CONCURRENT_REQUESTS = 5;
constexpr int BASE_TIMEOUT_MS = 2000;
auto sendUdpRequestWithTimeout = [&](const QByteArray &query, int requestTimeoutMs) -> QByteArray {
QUdpSocket socket;
const qint64 written = socket.writeDatagram(query, dnsAddress, port);
if (written != query.size()) {
return QByteArray();
}
QElapsedTimer timer;
timer.start();
while (timer.elapsed() < requestTimeoutMs) {
if (socket.waitForReadyRead(qMax(1, requestTimeoutMs - static_cast<int>(timer.elapsed())))) {
while (socket.hasPendingDatagrams()) {
QNetworkDatagram datagram = socket.receiveDatagram();
if (datagram.isValid()) {
return datagram.data();
}
}
}
}
return QByteArray();
};
auto sendWithRetry = [&](const QByteArray &query, int maxRetries) -> QByteArray {
for (int attempt = 0; attempt < maxRetries; ++attempt) {
const int timeout = BASE_TIMEOUT_MS * (attempt + 1);
QByteArray response = sendUdpRequestWithTimeout(query, timeout);
if (!response.isEmpty()) {
return response;
}
if (attempt < maxRetries - 1) {
QThread::msleep(timeout / 2);
}
}
return QByteArray();
};
const quint16 transactionId = static_cast<quint16>(QDateTime::currentMSecsSinceEpoch() & 0xFFFF);
const QByteArray initialQuery = buildDnsTxtQueryWithPayload(queryName, transactionId, payload);
qDebug() << "[DNS-UDP] initialQuery size=" << initialQuery.size() << "txid=" << transactionId;
if (initialQuery.isEmpty()) {
qWarning() << "[DNS-UDP] failed to build initial query (payload too big or queryName invalid)";
return QByteArray();
}
const QByteArray firstResponse = sendWithRetry(initialQuery, MAX_INITIAL_RETRIES);
qDebug() << "[DNS-UDP] first response size=" << firstResponse.size();
if (firstResponse.isEmpty()) {
qWarning() << "[DNS-UDP] no response from server after" << MAX_INITIAL_RETRIES << "retries";
return QByteArray();
}
const ChunkMeta meta = parseChunkMeta(firstResponse);
const QByteArray firstTxtData = parseDnsTxtResponse(firstResponse);
qDebug() << "[DNS-UDP] meta totalChunks=" << meta.totalChunks
<< "chunkId=" << meta.chunkId << "firstTxtData size=" << firstTxtData.size();
if (firstTxtData.isEmpty()) {
qWarning() << "[DNS-UDP] failed to parse TXT data from first response";
return QByteArray();
}
if (meta.totalChunks <= 1) {
qDebug() << "[DNS-UDP] single chunk, returning" << firstTxtData.size() << "bytes";
return firstTxtData;
}
QMap<int, QByteArray> chunks;
chunks[0] = firstTxtData;
auto requestChunksBatch = [&](const QList<int> &chunkIndices, int batchTimeout) {
if (chunkIndices.isEmpty()) return;
QList<QSharedPointer<QUdpSocket>> sockets;
QMap<QUdpSocket *, int> socketToIndex;
for (int idx : chunkIndices) {
if (chunks.contains(idx)) continue;
const quint16 chunkTxId =
static_cast<quint16>((QDateTime::currentMSecsSinceEpoch() + idx) & 0xFFFF);
const QByteArray chunkQuery =
buildDnsChunkRequest(queryName, chunkTxId, meta.chunkId, idx);
if (chunkQuery.isEmpty()) {
continue;
}
auto socket = QSharedPointer<QUdpSocket>::create();
socket->writeDatagram(chunkQuery, dnsAddress, port);
socketToIndex[socket.data()] = idx;
sockets.append(socket);
}
if (sockets.isEmpty()) return;
QElapsedTimer deadline;
deadline.start();
int receivedCount = 0;
const int expectedCount = sockets.size();
while (deadline.elapsed() < batchTimeout && receivedCount < expectedCount
&& chunks.size() < meta.totalChunks) {
for (auto &socket : sockets) {
if (socket->waitForReadyRead(50)) {
while (socket->hasPendingDatagrams()) {
QNetworkDatagram datagram = socket->receiveDatagram();
if (datagram.isValid()) {
const QByteArray chunkTxtData = parseDnsTxtResponse(datagram.data());
if (!chunkTxtData.isEmpty()) {
const ChunkMeta chunkMeta = parseChunkMeta(datagram.data());
const int idx = (chunkMeta.totalChunks > 0)
? chunkMeta.chunkIndex
: socketToIndex.value(socket.data(), -1);
if (idx >= 0 && !chunks.contains(idx)) {
chunks[idx] = chunkTxtData;
receivedCount++;
}
}
}
}
}
}
}
};
const int totalTimeout = qMax(timeoutMsecs / 2, 5000);
const int batchTimeout = totalTimeout / (MAX_CHUNK_RETRIES + 1);
for (int retryRound = 0; retryRound <= MAX_CHUNK_RETRIES; ++retryRound) {
QList<int> missing;
for (int i = 1; i < meta.totalChunks; ++i) {
if (!chunks.contains(i)) {
missing.append(i);
}
}
if (missing.isEmpty()) {
break;
}
for (int batchStart = 0; batchStart < missing.size(); batchStart += MAX_CONCURRENT_REQUESTS) {
const QList<int> batch = missing.mid(batchStart, MAX_CONCURRENT_REQUESTS);
requestChunksBatch(batch, batchTimeout);
}
}
QList<int> finalMissing;
for (int i = 0; i < meta.totalChunks; ++i) {
if (!chunks.contains(i)) {
finalMissing.append(i);
}
}
if (!finalMissing.isEmpty()) {
return QByteArray();
}
QByteArray combined;
combined.reserve(meta.totalSize > 0 ? meta.totalSize : meta.totalChunks * 500);
for (int i = 0; i < meta.totalChunks; ++i) {
combined.append(chunks[i]);
}
return combined;
}
} // namespace amnezia::transport::dns::DnsTunnel
-35
View File
@@ -1,35 +0,0 @@
#ifndef DNSTUNNEL_H
#define DNSTUNNEL_H
#include <QByteArray>
#include <QString>
#include "dnsResolver.h"
namespace amnezia::transport::dns::DnsTunnel
{
QByteArray send(const QByteArray &payload,
const QString &endpointName,
const QString &baseDomain,
const QString &dnsServer,
DnsProtocol protocol,
quint16 port,
int timeoutMsecs = 30000,
const QString &dohEndpoint = QStringLiteral("/dns-query"));
QByteArray sendOverUdp(const QByteArray &payload, const QString &queryName,
const QString &dnsServer, quint16 port, int timeoutMsecs);
QByteArray sendOverTcp(const QByteArray &payload, const QString &queryName,
const QString &dnsServer, quint16 port, int timeoutMsecs);
QByteArray sendOverTls(const QByteArray &payload, const QString &queryName,
const QString &dnsServer, quint16 port, int timeoutMsecs);
QByteArray sendOverHttps(const QByteArray &payload, const QString &queryName,
const QString &dnsServer, quint16 port, const QString &endpoint, int timeoutMsecs);
QByteArray sendOverUdpChunked(const QByteArray &payload, const QString &queryName,
const QString &dnsServer, quint16 port, int timeoutMsecs);
} // namespace amnezia::transport::dns::DnsTunnel
#endif // DNSTUNNEL_H
@@ -1,157 +0,0 @@
#include "dnsGatewayTransport.h"
#include <QDebug>
#include <QHostAddress>
#include <QHostInfo>
#include <QSharedPointer>
#include <QStringList>
#include "dns/dnsTunnel.h"
#include "core/networkUtilities.h"
#ifdef AMNEZIA_DESKTOP
#include "core/ipcclient.h"
#endif
namespace amnezia::transport
{
DnsGatewayTransport::DnsGatewayTransport(dns::DnsProtocol protocol,
const QString &dnsServer,
const QString &baseDomain,
quint16 port,
int timeoutMsecs,
bool isStrictKillSwitchEnabled,
const QString &dohEndpoint)
: m_protocol(protocol),
m_dnsServer(dnsServer),
m_baseDomain(baseDomain),
m_port(port),
m_timeoutMsecs(timeoutMsecs),
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled),
m_dohEndpoint(dohEndpoint)
{
}
QString DnsGatewayTransport::name() const
{
switch (m_protocol) {
case dns::DnsProtocol::Udp: return QStringLiteral("DNS-UDP");
case dns::DnsProtocol::Tcp: return QStringLiteral("DNS-TCP");
case dns::DnsProtocol::Tls: return QStringLiteral("DNS-DoT");
case dns::DnsProtocol::Https: return QStringLiteral("DNS-DoH");
case dns::DnsProtocol::Quic: return QStringLiteral("DNS-DoQ");
}
return QStringLiteral("DNS");
}
QString DnsGatewayTransport::resolveServerOnce()
{
if (m_resolved.load()) {
return m_resolvedServerIp;
}
QHostAddress addr(m_dnsServer);
if (!addr.isNull()) {
m_resolvedServerIp = m_dnsServer;
} else {
QHostInfo info = QHostInfo::fromName(m_dnsServer);
if (!info.addresses().isEmpty()) {
m_resolvedServerIp = info.addresses().first().toString();
} else {
m_resolvedServerIp = m_dnsServer;
}
}
m_resolved.store(true);
return m_resolvedServerIp;
}
void DnsGatewayTransport::applyKillSwitchAllowlist(const QString &ip)
{
#ifdef AMNEZIA_DESKTOP
if (!m_isStrictKillSwitchEnabled || ip.isEmpty()) {
return;
}
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
QRemoteObjectPendingReply<bool> reply = iface->addKillSwitchAllowedRange(QStringList { ip });
if (!reply.waitForFinished(1000) || !reply.returnValue()) {
qWarning() << "DnsGatewayTransport: addKillSwitchAllowedRange failed for" << ip;
}
});
#else
Q_UNUSED(ip)
#endif
}
amnezia::ErrorCode DnsGatewayTransport::send(const QString &endpointTemplate,
const QByteArray &requestBody,
QByteArray &decryptedResponse,
const DecryptionHook &decryptionHook)
{
QString endpointName = endpointTemplate;
endpointName.remove("%1");
if (endpointName.startsWith(QLatin1String("v1/"))) {
endpointName = endpointName.mid(3);
}
while (endpointName.endsWith(QLatin1Char('/'))) {
endpointName.chop(1);
}
while (endpointName.startsWith(QLatin1Char('/'))) {
endpointName = endpointName.mid(1);
}
qDebug() << "[DNS-Transport]" << name() << "send() endpointTemplate=" << endpointTemplate
<< "endpointName=" << endpointName << "baseDomain=" << m_baseDomain
<< "server=" << m_dnsServer << "port=" << m_port
<< "dohPath=" << m_dohEndpoint << "timeoutMs=" << m_timeoutMsecs
<< "requestBodyBytes=" << requestBody.size();
if (endpointName.isEmpty() || m_baseDomain.isEmpty() || m_dnsServer.isEmpty()) {
qWarning() << "[DNS-Transport] ABORT: empty endpoint/baseDomain/server";
return amnezia::ErrorCode::AmneziaServiceConnectionFailed;
}
const bool needsHostname = (m_protocol == dns::DnsProtocol::Tls
|| m_protocol == dns::DnsProtocol::Https);
QString serverIp = resolveServerOnce();
QString serverForRequest = needsHostname ? m_dnsServer : serverIp;
qDebug() << "[DNS-Transport] resolved server IP=" << serverIp
<< "serverForRequest=" << serverForRequest
<< "needsHostname=" << needsHostname;
applyKillSwitchAllowlist(serverIp);
const QByteArray encrypted = dns::DnsTunnel::send(requestBody,
endpointName,
m_baseDomain,
serverForRequest,
m_protocol,
m_port,
m_timeoutMsecs,
m_dohEndpoint);
qDebug() << "[DNS-Transport] DnsTunnel::send returned" << encrypted.size() << "bytes";
if (encrypted.isEmpty()) {
qWarning() << "[DNS-Transport] DnsTunnel returned empty payload, treat as connection failure";
return amnezia::ErrorCode::AmneziaServiceConnectionFailed;
}
if (!decryptionHook) {
qCritical() << "[DNS-Transport] decryption hook is null";
return amnezia::ErrorCode::ApiConfigDecryptionError;
}
DecryptionResult decrypted = decryptionHook(encrypted);
if (!decrypted.isOk) {
qCritical() << "[DNS-Transport] response decryption failed (encrypted bytes="
<< encrypted.size() << ")";
return amnezia::ErrorCode::ApiConfigDecryptionError;
}
qDebug() << "[DNS-Transport] success, decrypted response bytes=" << decrypted.decrypted.size();
decryptedResponse = decrypted.decrypted;
return amnezia::ErrorCode::NoError;
}
} // namespace amnezia::transport
@@ -1,49 +0,0 @@
#ifndef DNSGATEWAYTRANSPORT_H
#define DNSGATEWAYTRANSPORT_H
#include <QString>
#include <atomic>
#include "dns/dnsResolver.h"
#include "igatewaytransport.h"
namespace amnezia::transport
{
class DnsGatewayTransport : public IGatewayTransport
{
public:
DnsGatewayTransport(dns::DnsProtocol protocol,
const QString &dnsServer,
const QString &baseDomain,
quint16 port,
int timeoutMsecs,
bool isStrictKillSwitchEnabled,
const QString &dohEndpoint = QStringLiteral("/dns-query"));
QString name() const override;
amnezia::ErrorCode send(const QString &endpointTemplate,
const QByteArray &requestBody,
QByteArray &decryptedResponse,
const DecryptionHook &decryptionHook) override;
private:
QString resolveServerOnce();
void applyKillSwitchAllowlist(const QString &ip);
dns::DnsProtocol m_protocol;
QString m_dnsServer;
QString m_baseDomain;
quint16 m_port;
int m_timeoutMsecs;
bool m_isStrictKillSwitchEnabled;
QString m_dohEndpoint;
std::atomic_bool m_resolved{ false };
QString m_resolvedServerIp;
};
} // namespace amnezia::transport
#endif // DNSGATEWAYTRANSPORT_H
@@ -1,345 +0,0 @@
#include "httpGatewayTransport.h"
#include <algorithm>
#include <random>
#include <QCryptographicHash>
#include <QDebug>
#include <QEventLoop>
#include <QHostAddress>
#include <QJsonArray>
#include <QJsonDocument>
#include <QMutexLocker>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QSharedPointer>
#include <QThread>
#include <QUrl>
#include <QUuid>
#include "QBlockCipher.h"
#include "amnezia_application.h"
#include "core/api/apiUtils.h"
#include "core/networkUtilities.h"
#include "utilities.h"
#ifdef AMNEZIA_DESKTOP
#include "core/ipcclient.h"
#endif
#ifdef Q_OS_IOS
#include "platforms/ios/ios_controller.h"
#endif
namespace amnezia::transport
{
QMutex HttpGatewayTransport::s_proxyMutex;
QString HttpGatewayTransport::s_proxyUrl;
namespace
{
constexpr int kProxyHealthTimeoutMsecs = 1000;
constexpr int httpStatusCodeNotFound = 404;
constexpr int httpStatusCodeConflict = 409;
constexpr int httpStatusCodeNotImplemented = 501;
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 updateRequestResponsePattern("client version update is required");
} // namespace
HttpGatewayTransport::HttpGatewayTransport(const QString &endpoint,
bool isDevEnvironment,
int requestTimeoutMsecs,
bool isStrictKillSwitchEnabled)
: m_endpoint(endpoint),
m_isDevEnvironment(isDevEnvironment),
m_requestTimeoutMsecs(requestTimeoutMsecs),
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled)
{
}
void HttpGatewayTransport::applyKillSwitchAllowlist(const QString &host)
{
#ifdef AMNEZIA_DESKTOP
if (!m_isStrictKillSwitchEnabled || host.isEmpty()) {
return;
}
const QString ip = NetworkUtilities::getIPAddress(host);
if (ip.isEmpty()) {
return;
}
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
QRemoteObjectPendingReply<bool> reply = iface->addKillSwitchAllowedRange(QStringList { ip });
if (!reply.waitForFinished(1000) || !reply.returnValue()) {
qWarning() << "HttpGatewayTransport: addKillSwitchAllowedRange failed for" << ip;
}
});
#else
Q_UNUSED(host)
#endif
}
HttpGatewayTransport::ReplyOutcome HttpGatewayTransport::doPost(const QString &fullUrl, const QByteArray &requestBody)
{
ReplyOutcome outcome;
#ifdef Q_OS_IOS
IosController::Instance()->requestInetAccess();
QThread::msleep(10);
#endif
QNetworkRequest request;
request.setTransferTimeout(m_requestTimeoutMsecs);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("X-Client-Request-ID",
QUuid::createUuid().toString(QUuid::WithoutBraces).toUtf8());
request.setUrl(fullUrl);
applyKillSwitchAllowlist(QUrl(fullUrl).host());
QNetworkReply *reply = amnApp->networkManager()->post(request, requestBody);
QEventLoop wait;
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
QObject::connect(reply, &QNetworkReply::sslErrors, [&, reply](const QList<QSslError> &errors) {
outcome.sslErrors = errors;
#ifdef AGW_INSECURE_SSL
qWarning() << "[HTTP] sslErrors (ignored, AGW_INSECURE_SSL=1):" << errors;
reply->ignoreSslErrors();
outcome.sslErrors.clear();
#endif
});
wait.exec(QEventLoop::ExcludeUserInputEvents);
outcome.encryptedBody = reply->readAll();
outcome.errorString = reply->errorString();
outcome.networkError = reply->error();
outcome.httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
reply->deleteLater();
return outcome;
}
bool HttpGatewayTransport::shouldBypass(const ReplyOutcome &outcome, const DecryptionResult &decrypted) const
{
if (!outcome.sslErrors.isEmpty()) {
return false;
}
if (!decrypted.isOk) {
return true;
}
int apiHttpStatus = -1;
QJsonDocument jsonDoc = QJsonDocument::fromJson(decrypted.decrypted);
if (jsonDoc.isObject()) {
apiHttpStatus = jsonDoc.object().value("http_status").toInt(-1);
}
if (outcome.networkError == QNetworkReply::NetworkError::OperationCanceledError
|| outcome.networkError == QNetworkReply::NetworkError::TimeoutError) {
return true;
}
if (decrypted.decrypted.contains("html")) {
return true;
}
if (apiHttpStatus == httpStatusCodeNotFound) {
if (decrypted.decrypted.contains(errorResponsePattern1)
|| decrypted.decrypted.contains(errorResponsePattern2)
|| decrypted.decrypted.contains(errorResponsePattern3)) {
return false;
}
return true;
}
if (apiHttpStatus == httpStatusCodeNotImplemented) {
if (decrypted.decrypted.contains(updateRequestResponsePattern)) {
return false;
}
return true;
}
if (apiHttpStatus == httpStatusCodeConflict) {
return false;
}
if (outcome.networkError != QNetworkReply::NetworkError::NoError) {
return true;
}
return false;
}
QStringList HttpGatewayTransport::fetchProxyUrls(const QByteArray &/*serviceHint*/)
{
QStringList baseUrls = m_isDevEnvironment
? QString(DEV_S3_ENDPOINT).split(", ")
: QString(PROD_S3_ENDPOINT).split(", ");
QByteArray rsaKey = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
QStringList proxyStorageUrls;
for (const auto &baseUrl : baseUrls) {
proxyStorageUrls.push_back(baseUrl + "endpoints.json");
}
QNetworkRequest request;
request.setTransferTimeout(m_requestTimeoutMsecs);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
for (const auto &proxyStorageUrl : proxyStorageUrls) {
request.setUrl(proxyStorageUrl);
QNetworkReply *reply = amnApp->networkManager()->get(request);
QEventLoop wait;
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
wait.exec(QEventLoop::ExcludeUserInputEvents);
if (reply->error() != QNetworkReply::NoError) {
reply->deleteLater();
continue;
}
QByteArray encryptedResponseBody = reply->readAll();
reply->deleteLater();
QByteArray responseBody;
try {
if (!m_isDevEnvironment) {
QCryptographicHash hash(QCryptographicHash::Sha512);
hash.addData(rsaKey);
QByteArray hashResult = hash.result().toHex();
QByteArray key = QByteArray::fromHex(hashResult.left(64));
QByteArray iv = QByteArray::fromHex(hashResult.mid(64, 32));
QSimpleCrypto::QBlockCipher blockCipher;
responseBody = blockCipher.decryptAesBlockCipher(QByteArray::fromBase64(encryptedResponseBody), key, iv);
} else {
responseBody = encryptedResponseBody;
}
} catch (...) {
Utils::logException();
qCritical() << "HttpGatewayTransport: error decrypting proxy storage payload";
continue;
}
QJsonArray endpointsArray = QJsonDocument::fromJson(responseBody).array();
QStringList endpoints;
endpoints.reserve(endpointsArray.size());
for (const QJsonValue &endpoint : endpointsArray) {
endpoints.push_back(endpoint.toString());
}
return endpoints;
}
return {};
}
amnezia::ErrorCode HttpGatewayTransport::send(const QString &endpointTemplate,
const QByteArray &requestBody,
QByteArray &decryptedResponse,
const DecryptionHook &decryptionHook)
{
auto buildOutcome = [&](const QString &gatewayBase) {
return doPost(endpointTemplate.arg(gatewayBase), requestBody);
};
auto tryDecrypt = [&](const QByteArray &encrypted) -> DecryptionResult {
if (!decryptionHook) {
DecryptionResult r;
r.decrypted = encrypted;
r.isOk = false;
return r;
}
return decryptionHook(encrypted);
};
QString cachedProxy;
{
QMutexLocker lock(&s_proxyMutex);
cachedProxy = s_proxyUrl;
}
const QString primaryBase = cachedProxy.isEmpty() ? m_endpoint : cachedProxy;
ReplyOutcome outcome = buildOutcome(primaryBase);
DecryptionResult decrypted = tryDecrypt(outcome.encryptedBody);
if (outcome.sslErrors.isEmpty() && shouldBypass(outcome, decrypted)) {
QStringList proxyUrls = fetchProxyUrls(QByteArray());
std::random_device randomDevice;
std::mt19937 generator(randomDevice());
std::shuffle(proxyUrls.begin(), proxyUrls.end(), generator);
bool bypassResolved = false;
if (cachedProxy.isEmpty()) {
QNetworkRequest healthRequest;
healthRequest.setTransferTimeout(kProxyHealthTimeoutMsecs);
healthRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
for (const QString &proxyUrl : std::as_const(proxyUrls)) {
healthRequest.setUrl(proxyUrl + "lmbd-health");
QNetworkReply *reply = amnApp->networkManager()->get(healthRequest);
QEventLoop wait;
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
wait.exec(QEventLoop::ExcludeUserInputEvents);
const auto err = reply->error();
reply->deleteLater();
if (err == QNetworkReply::NoError) {
QMutexLocker lock(&s_proxyMutex);
s_proxyUrl = proxyUrl;
cachedProxy = proxyUrl;
break;
}
}
}
if (!cachedProxy.isEmpty()) {
ReplyOutcome retry = buildOutcome(cachedProxy);
DecryptionResult retryDecrypted = tryDecrypt(retry.encryptedBody);
if (retry.sslErrors.isEmpty() && !shouldBypass(retry, retryDecrypted)) {
outcome = retry;
decrypted = retryDecrypted;
bypassResolved = true;
}
}
if (!bypassResolved) {
for (const QString &proxyUrl : std::as_const(proxyUrls)) {
ReplyOutcome retry = buildOutcome(proxyUrl);
DecryptionResult retryDecrypted = tryDecrypt(retry.encryptedBody);
if (retry.sslErrors.isEmpty() && !shouldBypass(retry, retryDecrypted)) {
{
QMutexLocker lock(&s_proxyMutex);
s_proxyUrl = proxyUrl;
}
outcome = retry;
decrypted = retryDecrypted;
bypassResolved = true;
break;
}
}
}
}
auto errorCode = apiUtils::checkNetworkReplyErrors(outcome.sslErrors,
outcome.errorString,
outcome.networkError,
outcome.httpStatusCode,
decrypted.decrypted);
if (errorCode != amnezia::ErrorCode::NoError) {
return errorCode;
}
if (!decrypted.isOk) {
qCritical() << "HttpGatewayTransport: response decryption failed";
return amnezia::ErrorCode::ApiConfigDecryptionError;
}
decryptedResponse = decrypted.decrypted;
return amnezia::ErrorCode::NoError;
}
} // namespace amnezia::transport
@@ -1,58 +0,0 @@
#ifndef HTTPGATEWAYTRANSPORT_H
#define HTTPGATEWAYTRANSPORT_H
#include <QByteArray>
#include <QList>
#include <QMutex>
#include <QNetworkReply>
#include <QSslError>
#include <QString>
#include <QStringList>
#include "igatewaytransport.h"
namespace amnezia::transport
{
class HttpGatewayTransport : public IGatewayTransport
{
public:
HttpGatewayTransport(const QString &endpoint,
bool isDevEnvironment,
int requestTimeoutMsecs,
bool isStrictKillSwitchEnabled);
QString name() const override { return QStringLiteral("HTTP"); }
amnezia::ErrorCode send(const QString &endpointTemplate,
const QByteArray &requestBody,
QByteArray &decryptedResponse,
const DecryptionHook &decryptionHook) override;
private:
struct ReplyOutcome
{
QByteArray encryptedBody;
QList<QSslError> sslErrors;
QNetworkReply::NetworkError networkError = QNetworkReply::NoError;
QString errorString;
int httpStatusCode = 0;
};
ReplyOutcome doPost(const QString &fullUrl, const QByteArray &requestBody);
void applyKillSwitchAllowlist(const QString &host);
QStringList fetchProxyUrls(const QByteArray &serviceHint);
bool shouldBypass(const ReplyOutcome &outcome, const DecryptionResult &decrypted) const;
QString m_endpoint;
bool m_isDevEnvironment;
int m_requestTimeoutMsecs;
bool m_isStrictKillSwitchEnabled;
static QMutex s_proxyMutex;
static QString s_proxyUrl;
};
} // namespace amnezia::transport
#endif // HTTPGATEWAYTRANSPORT_H
-36
View File
@@ -1,36 +0,0 @@
#ifndef IGATEWAYTRANSPORT_H
#define IGATEWAYTRANSPORT_H
#include <QByteArray>
#include <QString>
#include <functional>
#include "core/defs.h"
namespace amnezia::transport
{
struct DecryptionResult
{
QByteArray decrypted;
bool isOk = false;
};
using DecryptionHook = std::function<DecryptionResult(const QByteArray &encrypted)>;
class IGatewayTransport
{
public:
virtual ~IGatewayTransport() = default;
virtual QString name() const = 0;
virtual amnezia::ErrorCode send(const QString &endpointTemplate,
const QByteArray &requestBody,
QByteArray &decryptedResponse,
const DecryptionHook &decryptionHook) = 0;
};
} // namespace amnezia::transport
#endif // IGATEWAYTRANSPORT_H
-44
View File
@@ -1,44 +0,0 @@
{
"primary": "http",
"retry_count": 3,
"timeout_ms": 10000,
"http": {
"enabled": true,
"endpoint": "https://your-gateway.example.com/"
},
"dns_transports": [
{
"type": "udp",
"server": "your-gateway.example.com",
"domain": "gateway.example.com",
"port": 5453
},
{
"type": "tcp",
"server": "your-gateway.example.com",
"domain": "gateway.example.com",
"port": 5453
},
{
"type": "dot",
"server": "your-gateway.example.com",
"domain": "gateway.example.com",
"port": 8853
},
{
"type": "doh",
"server": "your-gateway.example.com",
"domain": "gateway.example.com",
"port": 443,
"path": "/dns-query"
},
{
"type": "doq",
"server": "your-gateway.example.com",
"domain": "gateway.example.com",
"port": 8854
}
]
}
+54
View File
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleAllowMixedLocalizations</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>${QT_INTERNAL_DOLLAR_VAR}{PRODUCT_NAME}</string>
<key>CFBundleExecutable</key>
<string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
<key>CFBundleIdentifier</key>
<string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
<key>CFBundleVersion</key>
<string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
<key>NSHumanReadableCopyright</key>
<string>${MACOSX_BUNDLE_COPYRIGHT}</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>UIRequiredDeviceCapabilities</key>
<array/>
<key>UIRequiresFullScreen</key>
<true/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UILaunchStoryboardName</key>
<string>AmneziaVPNLaunchScreen</string>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
<key>com.wireguard.ios.app_group_id</key>
<string>group.org.amnezia.AmneziaVPN</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
</dict>
</plist>
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder.AppleTV.Storyboard" version="3.0" toolsVersion="13122.16" targetRuntime="AppleTV" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="1920" height="1080"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<viewLayoutGuide key="safeArea" id="wu6-TO-1qx"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>
+89 -32
View File
@@ -1,6 +1,14 @@
enable_language(Swift)
set(CLIENT_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/../..)
set(AMNEZIA_THIRDPARTY_ROOT "${CLIENT_ROOT_DIR}/3rd" CACHE PATH "Path to Amnezia client/3rd sources")
set(AMNEZIA_IOS_APPLETV ${AMNEZIA_IOS_ENABLE_APPLETV_TARGET})
if(AMNEZIA_IOS_APPLETV)
message("Network Extension tvOS mode is ON")
else()
message("Network Extension tvOS mode is OFF")
endif()
add_executable(networkextension)
set_target_properties(networkextension PROPERTIES
@@ -28,6 +36,23 @@ set_target_properties(networkextension PROPERTIES
XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/../../Frameworks"
)
if(AMNEZIA_IOS_APPLETV)
set_target_properties(networkextension PROPERTIES
XCODE_ATTRIBUTE_SUPPORTED_PLATFORMS "appletvos appletvsimulator"
XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "3"
XCODE_ATTRIBUTE_TVOS_DEPLOYMENT_TARGET "${CMAKE_OSX_DEPLOYMENT_TARGET}"
XCODE_ATTRIBUTE_SDKROOT "appletvos"
XCODE_ATTRIBUTE_SDKROOT[sdk=appletvos*] "appletvos"
XCODE_ATTRIBUTE_SDKROOT[sdk=appletvsimulator*] "appletvsimulator"
XCODE_ATTRIBUTE_LIBRARY_SEARCH_PATHS "$(inherited) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)"
XCODE_ATTRIBUTE_LIBRARY_SEARCH_PATHS[sdk=appletvos*] "$(inherited) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)"
XCODE_ATTRIBUTE_LIBRARY_SEARCH_PATHS[sdk=appletvsimulator*] "$(inherited) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)"
XCODE_ATTRIBUTE_EXCLUDED_LIBRARY_SEARCH_PATHS "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS*.sdk/usr/lib/swift"
XCODE_ATTRIBUTE_EXCLUDED_FRAMEWORK_SEARCH_PATHS "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS*.sdk/System/Library/Frameworks"
LINKER_LANGUAGE Swift
)
endif()
if(DEPLOY)
set_target_properties(networkextension PROPERTIES
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Apple Distribution"
@@ -45,38 +70,49 @@ endif()
set_target_properties(networkextension PROPERTIES
XCODE_ATTRIBUTE_SWIFT_VERSION "5.0"
XCODE_ATTRIBUTE_CLANG_ENABLE_MODULES "YES"
XCODE_ATTRIBUTE_SWIFT_OBJC_BRIDGING_HEADER "${CMAKE_CURRENT_SOURCE_DIR}/WireGuardNetworkExtension-Bridging-Header.h"
XCODE_ATTRIBUTE_SWIFT_OPTIMIZATION_LEVEL "-Onone"
XCODE_ATTRIBUTE_SWIFT_PRECOMPILE_BRIDGING_HEADER "NO"
)
set_target_properties(networkextension PROPERTIES
XCODE_ATTRIBUTE_SWIFT_OBJC_BRIDGING_HEADER "${CMAKE_CURRENT_SOURCE_DIR}/WireGuardNetworkExtension-Bridging-Header.h"
)
set_target_properties("networkextension" PROPERTIES
XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "X7UJ388FXK"
)
find_library(FW_ASSETS_LIBRARY AssetsLibrary)
find_library(FW_MOBILE_CORE MobileCoreServices)
find_library(FW_UI_KIT UIKit)
find_library(FW_LIBRESOLV libresolv.9.tbd)
target_link_libraries(networkextension PRIVATE ${FW_ASSETS_LIBRARY})
target_link_libraries(networkextension PRIVATE ${FW_MOBILE_CORE})
target_link_libraries(networkextension PRIVATE ${FW_UI_KIT})
target_link_libraries(networkextension PRIVATE ${FW_LIBRESOLV})
if(NOT AMNEZIA_IOS_APPLETV)
target_link_libraries(networkextension PRIVATE ${FW_UI_KIT})
target_link_libraries(networkextension PRIVATE ${FW_LIBRESOLV})
else()
target_link_libraries(networkextension PRIVATE -lresolv)
endif()
target_compile_options(networkextension PRIVATE -DGROUP_ID=\"${BUILD_IOS_GROUP_IDENTIFIER}\")
target_compile_options(networkextension PRIVATE -DNETWORK_EXTENSION=1)
set(WG_APPLE_SOURCE_DIR ${CLIENT_ROOT_DIR}/3rd/amneziawg-apple/Sources)
set(WG_APPLE_SOURCE_DIR ${AMNEZIA_THIRDPARTY_ROOT}/amneziawg-apple/Sources)
target_sources(networkextension PRIVATE
set(NE_COMMON_SOURCES
${CLIENT_ROOT_DIR}/platforms/ios/NELogController.swift
${CLIENT_ROOT_DIR}/platforms/ios/Log.swift
${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift
${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider.swift
)
set(NE_WIREGUARD_SOURCES
${WG_APPLE_SOURCE_DIR}/WireGuardKit/WireGuardAdapter.swift
${WG_APPLE_SOURCE_DIR}/WireGuardKit/PacketTunnelSettingsGenerator.swift
${WG_APPLE_SOURCE_DIR}/WireGuardKit/DNSResolver.swift
${WG_APPLE_SOURCE_DIR}/WireGuardNetworkExtension/ErrorNotifier.swift
${WG_APPLE_SOURCE_DIR}/Shared/Keychain.swift
${WG_APPLE_SOURCE_DIR}/Shared/Model/TunnelConfiguration+WgQuickConfig.swift
${WG_APPLE_SOURCE_DIR}/Shared/FileManager+Extension.swift
${WG_APPLE_SOURCE_DIR}/Shared/Model/NETunnelProviderProtocol+Extension.swift
${WG_APPLE_SOURCE_DIR}/Shared/Model/TunnelConfiguration+WgQuickConfig.swift
${WG_APPLE_SOURCE_DIR}/Shared/Model/String+ArrayConversion.swift
${WG_APPLE_SOURCE_DIR}/WireGuardKit/TunnelConfiguration.swift
${WG_APPLE_SOURCE_DIR}/WireGuardKit/IPAddressRange.swift
@@ -84,24 +120,50 @@ target_sources(networkextension PRIVATE
${WG_APPLE_SOURCE_DIR}/WireGuardKit/DNSServer.swift
${WG_APPLE_SOURCE_DIR}/WireGuardKit/InterfaceConfiguration.swift
${WG_APPLE_SOURCE_DIR}/WireGuardKit/PeerConfiguration.swift
${WG_APPLE_SOURCE_DIR}/Shared/FileManager+Extension.swift
${WG_APPLE_SOURCE_DIR}/WireGuardKitC/x25519.c
${WG_APPLE_SOURCE_DIR}/WireGuardKit/Array+ConcurrentMap.swift
${WG_APPLE_SOURCE_DIR}/WireGuardKit/IPAddress+AddrInfo.swift
${WG_APPLE_SOURCE_DIR}/WireGuardKit/PrivateKey.swift
${CLIENT_ROOT_DIR}/platforms/ios/HevSocksTunnel.swift
${CLIENT_ROOT_DIR}/platforms/ios/NELogController.swift
${CLIENT_ROOT_DIR}/platforms/ios/Log.swift
${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift
${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider.swift
${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider+WireGuard.swift
${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider+OpenVPN.swift
${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider+Xray.swift
${CLIENT_ROOT_DIR}/platforms/ios/WGConfig.swift
)
set(NE_XRAY_SOURCES
${CLIENT_ROOT_DIR}/platforms/ios/HevSocksTunnel.swift
${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider+Xray.swift
${CLIENT_ROOT_DIR}/platforms/ios/XrayConfig.swift
)
set(NE_OPENVPN_SOURCES
${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider+OpenVPN.swift
)
set(NE_APPLE_GLUE_SOURCES
${CLIENT_ROOT_DIR}/platforms/ios/iosglue.mm
)
if(AMNEZIA_IOS_APPLETV)
list(APPEND NE_APPLE_GLUE_SOURCES
${CLIENT_ROOT_DIR}/platforms/ios/tvos_cgo_stubs.c
)
endif()
target_sources(networkextension PRIVATE ${NE_COMMON_SOURCES})
if(NOT AMNEZIA_IOS_APPLETV)
target_sources(networkextension PRIVATE
${NE_WIREGUARD_SOURCES}
${NE_OPENVPN_SOURCES}
${NE_XRAY_SOURCES}
${NE_APPLE_GLUE_SOURCES}
)
else()
target_sources(networkextension PRIVATE
${NE_WIREGUARD_SOURCES}
${NE_APPLE_GLUE_SOURCES}
)
endif()
target_sources(networkextension PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/PrivacyInfo.xcprivacy
)
@@ -110,21 +172,16 @@ set_property(TARGET networkextension APPEND PROPERTY RESOURCE
${CMAKE_CURRENT_SOURCE_DIR}/PrivacyInfo.xcprivacy
)
## Build wireguard-go-version.h
execute_process(
COMMAND go list -m golang.zx2c4.com/wireguard
WORKING_DIRECTORY ${CLIENT_ROOT_DIR}/3rd/wireguard-apple/Sources/WireGuardKitGo
OUTPUT_VARIABLE WG_VERSION_FULL
)
string(REGEX REPLACE ".*v\([0-9.]*\).*" "\\1" WG_VERSION_STRING 1.1.1)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/wireguard-go-version.h.in
${CMAKE_CURRENT_BINARY_DIR}/wireguard-go-version.h)
target_sources(networkextension PRIVATE
${CMAKE_CURRENT_BINARY_DIR}/wireguard-go-version.h)
target_include_directories(networkextension PRIVATE ${CLIENT_ROOT_DIR})
target_include_directories(networkextension PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(networkextension PRIVATE ${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/wireguard/ios/arm64/libwg-go.a)
find_package(awg-apple REQUIRED)
target_link_libraries(networkextension PRIVATE amnezia::awg-apple)
target_link_libraries(networkextension PRIVATE ${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/xray/HevSocks5Tunnel.xcframework)
if(NOT AMNEZIA_IOS_APPLETV)
find_package(openvpnadapter REQUIRED)
target_link_libraries(networkextension PRIVATE amnezia::openvpnadapter)
find_package(hev-socks5-tunnel REQUIRED)
target_link_libraries(networkextension PRIVATE heiher::hev-socks5-tunnel)
endif()
@@ -1,3 +0,0 @@
#ifndef WIREGUARD_GO_VERSION
#define WIREGUARD_GO_VERSION "@WG_VERSION_STRING@"
#endif // WIREGUARD_GO_VERSION
+6 -17
View File
@@ -114,25 +114,14 @@ set_property(TARGET AmneziaVPNNetworkExtension APPEND PROPERTY RESOURCE
${CMAKE_CURRENT_SOURCE_DIR}/PrivacyInfo.xcprivacy
)
## Build wireguard-go-version.h
execute_process(
COMMAND go list -m golang.zx2c4.com/wireguard
WORKING_DIRECTORY ${CLIENT_ROOT_DIR}/3rd/wireguard-apple/Sources/WireGuardKitGo
OUTPUT_VARIABLE WG_VERSION_FULL
)
string(REGEX REPLACE ".*v\([0-9.]*\).*" "\\1" WG_VERSION_STRING 1.1.1)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/wireguard-go-version.h.in
${CMAKE_CURRENT_BINARY_DIR}/wireguard-go-version.h)
target_sources(AmneziaVPNNetworkExtension PRIVATE
${CMAKE_CURRENT_BINARY_DIR}/wireguard-go-version.h)
target_include_directories(AmneziaVPNNetworkExtension PRIVATE ${CLIENT_ROOT_DIR})
target_include_directories(AmneziaVPNNetworkExtension PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(AmneziaVPNNetworkExtension PRIVATE ${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/wireguard/macos/universal2/libwg-go.a)
find_package(openvpnadapter REQUIRED)
target_link_libraries(AmneziaVPNNetworkExtension PRIVATE amnezia::openvpnadapter)
message(${CLIENT_ROOT_DIR})
message(${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/xray/HevSocks5Tunnel.xcframework/macos-arm64_x86_64/libhev-socks5-tunnel.a)
target_link_libraries(AmneziaVPNNetworkExtension PRIVATE ${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/xray/HevSocks5Tunnel.xcframework/macos-arm64_x86_64/libhev-socks5-tunnel.a)
find_package(awg-apple REQUIRED)
target_link_libraries(AmneziaVPNNetworkExtension PRIVATE amnezia::awg-apple)
target_include_directories(AmneziaVPNNetworkExtension PRIVATE ${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/xray/HevSocks5Tunnel.xcframework/macos-arm64_x86_64/Headers)
find_package(hev-socks5-tunnel REQUIRED)
target_link_libraries(AmneziaVPNNetworkExtension PRIVATE heiher::hev-socks5-tunnel)
@@ -1,3 +0,0 @@
#ifndef WIREGUARD_GO_VERSION
#define WIREGUARD_GO_VERSION "@WG_VERSION_STRING@"
#endif // WIREGUARD_GO_VERSION
+3 -4
View File
@@ -12,11 +12,11 @@
#include "Windows.h"
#endif
#if defined(Q_OS_IOS)
#if defined(Q_OS_IOS) || defined(Q_OS_TVOS)
#include "platforms/ios/QtAppDelegate-C-Interface.h"
#endif
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !defined(MACOS_NE)
bool isAnotherInstanceRunning()
{
QLocalSocket socket;
@@ -47,7 +47,7 @@ int main(int argc, char *argv[])
AmneziaApplication app(argc, argv);
OsSignalHandler::setup();
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !defined(MACOS_NE)
if (isAnotherInstanceRunning()) {
QTimer::singleShot(1000, &app, [&]() { app.quit(); });
return app.exec();
@@ -75,7 +75,6 @@ int main(int argc, char *argv[])
qInfo().noquote() << QString("Started %1 version %2 %3").arg(APPLICATION_NAME, APP_VERSION, GIT_COMMIT_HASH);
qInfo().noquote() << QString("%1 (%2)").arg(QSysInfo::prettyProductName(), QSysInfo::currentCpuArchitecture());
qInfo().noquote() << QString("SSL backend: %1").arg(QSslSocket::sslLibraryVersionString());
return app.exec();
}
+1 -6
View File
@@ -270,12 +270,7 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
&& !wgConfig.value(amnezia::config_key::initPacketMagicHeader).isUndefined()
&& !wgConfig.value(amnezia::config_key::responsePacketMagicHeader).isUndefined()
&& !wgConfig.value(amnezia::config_key::underloadPacketMagicHeader).isUndefined()
&& !wgConfig.value(amnezia::config_key::transportPacketMagicHeader).isUndefined()
&& !wgConfig.value(amnezia::config_key::specialJunk1).isUndefined()
&& !wgConfig.value(amnezia::config_key::specialJunk2).isUndefined()
&& !wgConfig.value(amnezia::config_key::specialJunk3).isUndefined()
&& !wgConfig.value(amnezia::config_key::specialJunk4).isUndefined()
&& !wgConfig.value(amnezia::config_key::specialJunk5).isUndefined()) {
&& !wgConfig.value(amnezia::config_key::transportPacketMagicHeader).isUndefined()) {
json.insert(amnezia::config_key::junkPacketCount, wgConfig.value(amnezia::config_key::junkPacketCount));
json.insert(amnezia::config_key::junkPacketMinSize, wgConfig.value(amnezia::config_key::junkPacketMinSize));
json.insert(amnezia::config_key::junkPacketMaxSize, wgConfig.value(amnezia::config_key::junkPacketMaxSize));
+3 -9
View File
@@ -72,9 +72,9 @@ void NetworkWatcher::initialize() {
connect(m_impl, &NetworkWatcherImpl::unsecuredNetwork, this,
&NetworkWatcher::unsecuredNetwork);
connect(m_impl, &NetworkWatcherImpl::networkChanged, this,
&NetworkWatcher::networkChange);
connect(m_impl, &NetworkWatcherImpl::sleepMode, this,
&NetworkWatcher::onSleepMode);
&NetworkWatcher::networkChanged);
connect(m_impl, &NetworkWatcherImpl::wakeup, this,
&NetworkWatcher::wakeup);
m_impl->initialize();
// Enable sleep/wake monitoring for VPN auto-reconnection
@@ -97,12 +97,6 @@ void NetworkWatcher::settingsChanged() {
logger.debug() << "NetworkWatcher settings changed - keeping sleep monitoring active";
}
void NetworkWatcher::onSleepMode()
{
logger.debug() << "Resumed from sleep mode";
emit sleepMode();
}
void NetworkWatcher::unsecuredNetwork(const QString& networkName,
const QString& networkId) {
logger.debug() << "Unsecured network:" << logger.sensitive(networkName)
+2 -4
View File
@@ -29,13 +29,11 @@ public:
// false to restore.
void simulateDisconnection(bool simulatedDisconnection);
void onSleepMode();
QNetworkInformation::Reachability getReachability();
signals:
void networkChange();
void sleepMode();
void networkChanged();
void wakeup();
private:
void settingsChanged();
+1 -1
View File
@@ -41,7 +41,7 @@ signals:
// TODO: Only windows-networkwatcher has this, the other plattforms should
// too.
void networkChanged(QString newBSSID);
void sleepMode();
void wakeup();
private:
@@ -3,7 +3,9 @@ import NetworkExtension
import Network
import os
import Darwin
#if !os(tvOS)
import OpenVPNAdapter
#endif
enum TunnelProtoType: String {
case wireguard, openvpn, xray
@@ -38,8 +40,10 @@ struct Constants {
class PacketTunnelProvider: NEPacketTunnelProvider {
var wgAdapter: WireGuardAdapter?
#if !os(tvOS)
var ovpnAdapter: OpenVPNAdapter?
private lazy var openVPNPacketFlowAdapter = PacketTunnelFlowAdapter(flow: packetFlow)
#endif
private let pathMonitorQueue = DispatchQueue(label: Constants.processQueueName + ".path-monitor")
private let pathMonitor = NWPathMonitor()
private var didReceiveInitialPathUpdate = false
@@ -49,7 +53,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
var splitTunnelType: Int?
var splitTunnelSites: [String]?
#if !os(tvOS)
let vpnReachability = OpenVPNReachability()
#endif
var startHandler: ((Error?) -> Void)?
var stopHandler: (() -> Void)?
@@ -57,9 +63,11 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
var activeIfaceIdx: UInt32 = 0
#if !os(tvOS)
func openVPNPacketFlow() -> OpenVPNAdapterPacketFlow {
openVPNPacketFlowAdapter
}
#endif
override init() {
super.init()
@@ -206,9 +214,21 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
errorNotifier: errorNotifier,
completionHandler: completionHandler)
case .openvpn:
#if os(tvOS)
completionHandler(NSError(domain: "org.amnezia.ne",
code: -1002,
userInfo: [NSLocalizedDescriptionKey: "OpenVPN backend is not available for tvOS in this build"]))
#else
startOpenVPN(completionHandler: completionHandler)
#endif
case .xray:
#if os(tvOS)
completionHandler(NSError(domain: "org.amnezia.ne",
code: -1003,
userInfo: [NSLocalizedDescriptionKey: "Xray backend is not available for tvOS in this build"]))
#else
startXray(completionHandler: completionHandler)
#endif
}
}
@@ -225,10 +245,18 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
stopWireguard(with: reason,
completionHandler: completionHandler)
case .openvpn:
#if os(tvOS)
completionHandler()
#else
stopOpenVPN(with: reason,
completionHandler: completionHandler)
#endif
case .xray:
#if os(tvOS)
completionHandler()
#else
stopXray(completionHandler: completionHandler)
#endif
}
}
@@ -242,7 +270,11 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
case .wireguard:
handleWireguardStatusMessage(messageData, completionHandler: completionHandler)
case .openvpn:
#if !os(tvOS)
handleOpenVPNStatusMessage(messageData, completionHandler: completionHandler)
#else
completionHandler?(nil)
#endif
case .xray:
break;
}
@@ -260,7 +292,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
private func handle(networkChange changePath: Network.NWPath, completion: @escaping (Error?) -> Void) {
updateActiveInterfaceIndex(for: changePath)
wg_log(.info, message: "Tunnel restarted.")
neLog(.info, message: "Tunnel restarted.")
startTunnel(options: nil, completionHandler: completion)
}
}
@@ -311,16 +343,17 @@ private extension PacketTunnelProvider {
}
extension WireGuardLogLevel {
var osLogLevel: OSLogType {
switch self {
case .verbose:
return .debug
case .error:
return .error
var osLogLevel: OSLogType {
switch self {
case .verbose:
return .debug
case .error:
return .error
}
}
}
}
#if !os(tvOS)
final class PacketTunnelFlowAdapter: NSObject, OpenVPNAdapterPacketFlow {
private let flow: NEPacketTunnelFlow
@@ -339,6 +372,7 @@ final class PacketTunnelFlowAdapter: NSObject, OpenVPNAdapterPacketFlow {
flow.writePackets(packets, withProtocols: protocols)
}
}
#endif
extension NEProviderStopReason {
var amneziaDescription: String {
+1 -1
View File
@@ -1,4 +1,4 @@
#if !MACOS_NE
#if !MACOS_NE && !TARGET_OS_TV
#include "QRCodeReaderBase.h"
#import <UIKit/UIKit.h>
+11 -5
View File
@@ -959,6 +959,10 @@ void IosController::sendVpnExtensionMessage(NSDictionary* message, std::function
}
bool IosController::shareText(const QStringList& filesToSend) {
#if defined(Q_OS_TVOS)
Q_UNUSED(filesToSend)
return false;
#else
NSMutableArray *sharingItems = [NSMutableArray new];
for (int i = 0; i < filesToSend.size(); i++) {
@@ -967,7 +971,7 @@ bool IosController::shareText(const QStringList& filesToSend) {
}
#if !MACOS_NE
UIViewController *qtController = getViewController();
if (!qtController) return;
if (!qtController) return false;
UIActivityViewController *activityController = [[UIActivityViewController alloc] initWithActivityItems:sharingItems applicationActivities:nil];
#endif
@@ -991,23 +995,25 @@ bool IosController::shareText(const QStringList& filesToSend) {
wait.exec();
return isAccepted;
#endif
}
QString IosController::openFile() {
#if !MACOS_NE
#if defined(Q_OS_TVOS)
return QString();
#elif !MACOS_NE
UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[@"public.item"] inMode:UIDocumentPickerModeOpen];
DocumentPickerDelegate *documentPickerDelegate = [[DocumentPickerDelegate alloc] init];
documentPicker.delegate = documentPickerDelegate;
UIViewController *qtController = getViewController();
if (!qtController) return;
if (!qtController) return QString();
[qtController presentViewController:documentPicker animated:YES completion:nil];
#endif
__block QString filePath;
#if !MACOS_NE
#if !MACOS_NE && !defined(Q_OS_TVOS)
documentPickerDelegate.documentPickerClosedCallback = ^(NSString *path) {
if (path) {
filePath = QString::fromUtf8(path.UTF8String);
@@ -1,6 +1,7 @@
#import <NetworkExtension/NetworkExtension.h>
#import <NetworkExtension/NETunnelProviderSession.h>
#import <Foundation/Foundation.h>
#include <TargetConditionals.h>
#if !MACOS_NE
#include <UIKit/UIKit.h>
@@ -21,7 +22,7 @@ class IosController;
@end
typedef void (^DocumentPickerClosedCallback)(NSString *path);
#if !MACOS_NE
#if !MACOS_NE && !TARGET_OS_TV
@interface DocumentPickerDelegate : NSObject <UIDocumentPickerDelegate>
@property (nonatomic, copy) DocumentPickerClosedCallback documentPickerClosedCallback;
@@ -26,7 +26,7 @@
@end
#if !MACOS_NE
#if !MACOS_NE && !TARGET_OS_TV
@implementation DocumentPickerDelegate
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls {
@@ -7,6 +7,24 @@
#import <UserNotifications/UserNotifications.h>
#import <Foundation/Foundation.h>
#if defined(Q_OS_TVOS)
IOSNotificationHandler::IOSNotificationHandler(QObject* parent) : NotificationHandler(parent) {}
IOSNotificationHandler::~IOSNotificationHandler() {}
void IOSNotificationHandler::notify(NotificationHandler::Message type,
const QString& title,
const QString& message,
int timerMsec) {
Q_UNUSED(type)
Q_UNUSED(title)
Q_UNUSED(message)
Q_UNUSED(timerMsec)
}
#else
#if !MACOS_NE
#import <UIKit/UIKit.h>
@@ -172,3 +190,5 @@ void IOSNotificationHandler::notify(NotificationHandler::Message type, const QSt
}];
}
#endif
#endif // Q_OS_TVOS
+6
View File
@@ -0,0 +1,6 @@
/*
* tvOS does not export these iOS runtime helpers used by Go cgo archives.
* WireGuardKitGo references them indirectly; provide no-op stubs for tvOS.
*/
void darwin_arm_init_mach_exception_handler(void) {}
void darwin_arm_init_thread_exception_port(void) {}
@@ -80,7 +80,7 @@ bool WireguardUtilsLinux::addInterface(const InterfaceConfig& config) {
QDir appPath(QCoreApplication::applicationDirPath());
QStringList wgArgs = {"-f", "amn0"};
m_tunnel.start(appPath.filePath("../../client/bin/wireguard-go"), wgArgs);
m_tunnel.start(appPath.filePath("amneziawg-go"), wgArgs);
if (!m_tunnel.waitForStarted(WG_TUN_PROC_TIMEOUT)) {
logger.error() << "Unable to start tunnel process due to timeout";
m_tunnel.kill();
@@ -41,8 +41,8 @@ void LinuxNetworkWatcher::initialize() {
connect(m_worker, &LinuxNetworkWatcherWorker::unsecuredNetwork, this,
&LinuxNetworkWatcher::unsecuredNetwork);
connect(m_worker, &LinuxNetworkWatcherWorker::sleepMode, this,
&NetworkWatcherImpl::sleepMode);
connect(m_worker, &LinuxNetworkWatcherWorker::wakeup, this,
&NetworkWatcherImpl::wakeup);
// Let's wait a few seconds to allow the UI to be fully loaded and shown.
// This is not strictly needed, but it's better for user experience because
@@ -200,7 +200,7 @@ void LinuxNetworkWatcherWorker::checkDevices() {
void LinuxNetworkWatcherWorker::NMStateChanged(quint32 state)
{
if (state == NM_STATE_ASLEEP) {
emit sleepMode();
emit wakeup();
}
logger.debug() << "NMStateChanged " << state;
@@ -23,7 +23,7 @@ class LinuxNetworkWatcherWorker final : public QObject {
signals:
void unsecuredNetwork(const QString& networkName, const QString& networkId);
void sleepMode();
void wakeup();
public slots:
void initialize();
@@ -79,7 +79,7 @@ bool WireguardUtilsMacos::addInterface(const InterfaceConfig& config) {
QDir appPath(QCoreApplication::applicationDirPath());
QStringList wgArgs = {"-f", "utun"};
m_tunnel.start(appPath.filePath("wireguard-go"), wgArgs);
m_tunnel.start(appPath.filePath("amneziawg-go"), wgArgs);
if (!m_tunnel.waitForStarted(WG_TUN_PROC_TIMEOUT)) {
logger.error() << "Unable to start tunnel process due to timeout";
m_tunnel.kill();
@@ -173,10 +173,10 @@ void PowerNotificationsListener::sleepWakeupCallBack(void *refParam, io_service_
case kIOMessageSystemHasPoweredOn:
/* Announces that the system and its devices have woken up. */
logger.debug() << "System has powered on - emitting sleepMode signal from dedicated CFRunLoop thread";
logger.debug() << "System has powered on - emitting wakeup signal from dedicated CFRunLoop thread";
if (listener->m_watcher) {
// Use QMetaObject::invokeMethod for thread-safe signal emission
QMetaObject::invokeMethod(listener->m_watcher, "sleepMode", Qt::QueuedConnection);
QMetaObject::invokeMethod(listener->m_watcher, "wakeup", Qt::QueuedConnection);
}
break;
@@ -62,6 +62,9 @@ void WindowsDaemon::prepareActivation(const InterfaceConfig& config, int inetAda
}
void WindowsDaemon::activateSplitTunnel(const InterfaceConfig& config, int vpnAdapterIndex) {
if (m_splitTunnelManager == nullptr)
return;
if (config.m_vpnDisabledApps.length() > 0) {
m_splitTunnelManager->start(m_inetAdapterIndex, vpnAdapterIndex);
m_splitTunnelManager->excludeApps(config.m_vpnDisabledApps);
@@ -41,7 +41,7 @@ LRESULT WindowsNetworkWatcher::PowerWndProcCallback(HWND hwnd, UINT uMsg, WPARAM
switch (uMsg) {
case WM_POWERBROADCAST:
if (wParam == PBT_APMRESUMESUSPEND) {
emit obj->sleepMode();
emit obj->wakeup();
}
break;
default:
+3 -9
View File
@@ -232,12 +232,6 @@ ErrorCode OpenVpnProtocol::start()
return ErrorCode::AmneziaServiceConnectionFailed;
}
m_openVpnProcess->waitForSource(5000);
if (!m_openVpnProcess->isInitialized()) {
qWarning() << "IpcProcess replica is not connected!";
setLastError(ErrorCode::AmneziaServiceConnectionFailed);
return ErrorCode::AmneziaServiceConnectionFailed;
}
m_openVpnProcess->setProgram(PermittedProcess::OpenVPN);
QStringList arguments({
"--config", configPath(), "--management", m_managementHost, QString::number(mgmtPort),
@@ -246,13 +240,13 @@ ErrorCode OpenVpnProtocol::start()
m_openVpnProcess->setArguments(arguments);
qDebug() << arguments.join(" ");
connect(m_openVpnProcess.data(), &PrivilegedProcess::errorOccurred,
connect(m_openVpnProcess.data(), &IpcProcessInterfaceReplica::errorOccurred,
[&](QProcess::ProcessError error) { qDebug() << "PrivilegedProcess errorOccurred" << error; });
connect(m_openVpnProcess.data(), &PrivilegedProcess::stateChanged,
connect(m_openVpnProcess.data(), &IpcProcessInterfaceReplica::stateChanged,
[&](QProcess::ProcessState newState) { qDebug() << "PrivilegedProcess stateChanged" << newState; });
connect(m_openVpnProcess.data(), &PrivilegedProcess::finished, this,
connect(m_openVpnProcess.data(), &IpcProcessInterfaceReplica::finished, this,
[&]() { setConnectionState(Vpn::ConnectionState::Disconnected); });
m_openVpnProcess->start();
+1 -1
View File
@@ -53,7 +53,7 @@ private:
void updateRouteGateway(QString line);
void updateVpnGateway(const QString &line);
QSharedPointer<PrivilegedProcess> m_openVpnProcess;
QSharedPointer<IpcProcessInterfaceReplica> m_openVpnProcess;
};
#endif // OPENVPNPROTOCOL_H
+3 -3
View File
@@ -190,7 +190,7 @@ namespace amnezia
constexpr char defaultPort[] = "51820";
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(MACOS_NE)
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(Q_OS_TVOS) || defined(MACOS_NE)
constexpr char defaultMtu[] = "1280";
#else
constexpr char defaultMtu[] = "1376";
@@ -210,7 +210,7 @@ namespace amnezia
namespace awg
{
constexpr char defaultPort[] = "55424";
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(MACOS_NE)
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(Q_OS_TVOS) || defined(MACOS_NE)
constexpr char defaultMtu[] = "1280";
#else
constexpr char defaultMtu[] = "1376";
@@ -233,7 +233,7 @@ namespace amnezia
constexpr char defaultResponsePacketMagicHeader[] = "3288052141";
constexpr char defaultTransportPacketMagicHeader[] = "2528465083";
constexpr char defaultUnderloadPacketMagicHeader[] = "1766607858";
constexpr char defaultSpecialJunk1[] = "<b 0x084481800001000300000000077469636b65747306776964676574096b696e6f706f69736b0272750000010001c00c0005000100000039001806776964676574077469636b6574730679616e646578c025c0390005000100000039002b1765787465726e616c2d7469636b6574732d776964676574066166697368610679616e646578036e657400c05d000100010000001c000457fafe25>";
constexpr char defaultSpecialJunk1[] = "<r 2><b 0x858000010001000000000669636c6f756403636f6d0000010001c00c000100010000105a00044d583737>";
constexpr char defaultSpecialJunk2[] = "";
constexpr char defaultSpecialJunk3[] = "";
constexpr char defaultSpecialJunk4[] = "";
+2 -2
View File
@@ -4,7 +4,7 @@
#include "core/errorstrings.h"
#include "vpnprotocol.h"
#if defined(Q_OS_WINDOWS) || defined(Q_OS_MACX) and !defined MACOS_NE || (defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID))
#if defined(Q_OS_WINDOWS) || (defined(Q_OS_MACX) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !defined(MACOS_NE)) || (defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID))
#include "openvpnovercloakprotocol.h"
#include "openvpnprotocol.h"
#include "shadowsocksvpnprotocol.h"
@@ -114,7 +114,7 @@ VpnProtocol *VpnProtocol::factory(DockerContainer container, const QJsonObject &
#if defined(Q_OS_WINDOWS)
case DockerContainer::Ipsec: return new Ikev2Protocol(configuration);
#endif
#if defined(Q_OS_WINDOWS) || defined(Q_OS_MACX) and !defined MACOS_NE || (defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID))
#if defined(Q_OS_WINDOWS) || (defined(Q_OS_MACX) && !defined(Q_OS_IOS) && !defined(Q_OS_TVOS) && !defined(MACOS_NE)) || (defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID))
case DockerContainer::OpenVpn: return new OpenVpnProtocol(configuration);
case DockerContainer::Cloak: return new OpenVpnOverCloakProtocol(configuration);
case DockerContainer::ShadowSocks: return new ShadowSocksVpnProtocol(configuration);
+2 -2
View File
@@ -15,7 +15,7 @@ WireguardProtocol::WireguardProtocol(const QJsonObject &configuration, QObject *
m_impl.reset(new LocalSocketController());
connect(m_impl.get(), &ControllerImpl::connected, this,
[this](const QString &pubkey, const QDateTime &connectionTimestamp) {
emit connectionStateChanged(Vpn::ConnectionState::Connected);
setConnectionState(Vpn::ConnectionState::Connected);
});
connect(m_impl.get(), &ControllerImpl::statusUpdated, this,
[this](const QString& serverIpv4Gateway,
@@ -38,7 +38,7 @@ WireguardProtocol::WireguardProtocol(const QJsonObject &configuration, QObject *
});
connect(m_impl.get(), &ControllerImpl::disconnected, this,
[this]() { emit connectionStateChanged(Vpn::ConnectionState::Disconnected); });
[this]() { setConnectionState(Vpn::ConnectionState::Disconnected); });
m_impl->initialize(nullptr, nullptr);
}
+196 -94
View File
@@ -1,6 +1,7 @@
#include "xrayprotocol.h"
#include "core/ipcclient.h"
#include "ipc.h"
#include "utilities.h"
#include "core/networkUtilities.h"
@@ -9,14 +10,37 @@
#include <QJsonObject>
#include <QNetworkInterface>
#include <QJsonDocument>
#include <QtCore/qlogging.h>
#include <QtCore/qobjectdefs.h>
#include <QtCore/qprocess.h>
#ifdef Q_OS_MACOS
static const QString tunName = "utun22";
#else
static const QString tunName = "tun2";
#endif
XrayProtocol::XrayProtocol(const QJsonObject &configuration, QObject *parent) : VpnProtocol(configuration, parent)
{
readXrayConfiguration(configuration);
m_routeGateway = NetworkUtilities::getGatewayAndIface().first;
m_vpnGateway = amnezia::protocols::xray::defaultLocalAddr;
m_vpnLocalAddress = amnezia::protocols::xray::defaultLocalAddr;
m_t2sProcess = IpcClient::InterfaceTun2Socks();
m_routeGateway = NetworkUtilities::getGatewayAndIface().first;
m_routeMode = static_cast<Settings::RouteMode>(configuration.value(amnezia::config_key::splitTunnelType).toInt());
m_remoteAddress = NetworkUtilities::getIPAddress(m_rawConfig.value(amnezia::config_key::hostName).toString());
const QString primaryDns = configuration.value(amnezia::config_key::dns1).toString();
m_dnsServers.push_back(QHostAddress(primaryDns));
if (primaryDns != amnezia::protocols::dns::amneziaDnsIp) {
const QString secondaryDns = configuration.value(amnezia::config_key::dns2).toString();
m_dnsServers.push_back(QHostAddress(secondaryDns));
}
QJsonObject xrayConfiguration = configuration.value(ProtocolProps::key_proto_config_data(Proto::Xray)).toObject();
if (xrayConfiguration.isEmpty()) {
xrayConfiguration = configuration.value(ProtocolProps::key_proto_config_data(Proto::SSXray)).toObject();
}
m_xrayConfig = xrayConfiguration;
}
XrayProtocol::~XrayProtocol()
@@ -29,72 +53,16 @@ ErrorCode XrayProtocol::start()
{
qDebug() << "XrayProtocol::start()";
const ErrorCode err = IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
iface->xrayStart(QJsonDocument(m_xrayConfig).toJson());
return ErrorCode::NoError;
return IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
auto xrayStart = iface->xrayStart(QJsonDocument(m_xrayConfig).toJson());
if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) {
qCritical() << "Failed to start xray";
return ErrorCode::XrayExecutableCrashed;
}
return startTun2Socks();
}, [] () {
return ErrorCode::AmneziaServiceConnectionFailed;
});
if (err != ErrorCode::NoError)
return err;
setConnectionState(Vpn::ConnectionState::Connecting);
return startTun2Sock();
}
ErrorCode XrayProtocol::startTun2Sock()
{
m_t2sProcess->start();
connect(m_t2sProcess.data(), &IpcProcessTun2SocksReplica::stateChanged, this,
[&](QProcess::ProcessState newState) { qDebug() << "PrivilegedProcess stateChanged" << newState; });
connect(m_t2sProcess.data(), &IpcProcessTun2SocksReplica::setConnectionState, this, [&](int vpnState) {
qDebug() << "PrivilegedProcess setConnectionState " << vpnState;
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
if (vpnState == Vpn::ConnectionState::Connected) {
setConnectionState(Vpn::ConnectionState::Connecting);
QList<QHostAddress> dnsAddr;
dnsAddr.push_back(QHostAddress(m_primaryDNS));
// We don't use secondary DNS if primary DNS is AmneziaDNS
if (!m_primaryDNS.contains(amnezia::protocols::dns::amneziaDnsIp)) {
dnsAddr.push_back(QHostAddress(m_secondaryDNS));
}
#ifdef Q_OS_WIN
QThread::msleep(8000);
#endif
#ifdef Q_OS_MACOS
QThread::msleep(5000);
iface->createTun("utun22", amnezia::protocols::xray::defaultLocalAddr);
iface->updateResolvers("utun22", dnsAddr);
#endif
#ifdef Q_OS_LINUX
QThread::msleep(1000);
iface->createTun("tun2", amnezia::protocols::xray::defaultLocalAddr);
iface->updateResolvers("tun2", dnsAddr);
#endif
if (m_routeMode == Settings::RouteMode::VpnAllSites) {
iface->routeAddList(m_vpnGateway, QStringList() << "1.0.0.0/8" << "2.0.0.0/7" << "4.0.0.0/6" << "8.0.0.0/5" << "16.0.0.0/4" << "32.0.0.0/3" << "64.0.0.0/2" << "128.0.0.0/1");
}
iface->StopRoutingIpv6();
#ifdef Q_OS_WIN
iface->updateResolvers("tun2", dnsAddr);
#endif
setConnectionState(Vpn::ConnectionState::Connected);
}
#if !defined(Q_OS_MACOS)
if (vpnState == Vpn::ConnectionState::Disconnected) {
setConnectionState(Vpn::ConnectionState::Disconnected);
iface->deleteTun("tun2");
iface->StartRoutingIpv6();
iface->clearSavedRoutes();
}
#endif
});
});
return ErrorCode::NoError;
}
void XrayProtocol::stop()
@@ -102,43 +70,177 @@ void XrayProtocol::stop()
qDebug() << "XrayProtocol::stop()";
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
#ifdef AMNEZIA_DESKTOP
QRemoteObjectPendingReply<bool> StartRoutingIpv6Resp = iface->StartRoutingIpv6();
if (!StartRoutingIpv6Resp.waitForFinished(1000)) {
qWarning() << "XrayProtocol::stop(): Failed to start routing ipv6";
}
auto disableKillSwitch = iface->disableKillSwitch();
if (!disableKillSwitch.waitForFinished() || !disableKillSwitch.returnValue())
qWarning() << "Failed to disable killswitch";
QRemoteObjectPendingReply<bool> restoreResolvers = iface->restoreResolvers();
if (!restoreResolvers.waitForFinished(1000)) {
qWarning() << "XrayProtocol::stop(): Failed to restore resolvers";
}
auto StartRoutingIpv6 = iface->StartRoutingIpv6();
if (!StartRoutingIpv6.waitForFinished() || !StartRoutingIpv6.returnValue())
qWarning() << "Failed to start routing ipv6";
#if !defined(Q_OS_MACOS)
QRemoteObjectPendingReply<bool> deleteTunResp = iface->deleteTun("tun2");
if (!deleteTunResp.waitForFinished(1000)) {
qWarning() << "XrayProtocol::stop(): Failed to delete tun";
}
#endif
#endif
iface->xrayStop();
auto restoreResolvers = iface->restoreResolvers();
if (!restoreResolvers.waitForFinished() || !restoreResolvers.returnValue())
qWarning() << "Failed to restore resolvers";
auto deleteTun = iface->deleteTun(tunName);
if (!deleteTun.waitForFinished() || !deleteTun.returnValue())
qWarning() << "Failed to delete tun";
auto xrayStop = iface->xrayStop();
if (!xrayStop.waitForFinished() || !xrayStop.returnValue())
qWarning() << "Failed to stop xray";
});
if (m_t2sProcess) {
m_t2sProcess->stop();
QThread::msleep(200);
if (m_tun2socksProcess) {
m_tun2socksProcess->blockSignals(true);
#ifndef Q_OS_WIN
m_tun2socksProcess->terminate();
auto waitForFinished = m_tun2socksProcess->waitForFinished(1000);
if (!waitForFinished.waitForFinished() || !waitForFinished.returnValue()) {
qWarning() << "Failed to terminate tun2socks. Killing the process...";
m_tun2socksProcess->kill();
}
#else
// terminate does not do anything useful on Windows
// so just kill the process
m_tun2socksProcess->kill();
#endif
m_tun2socksProcess->close();
m_tun2socksProcess.reset();
}
setConnectionState(Vpn::ConnectionState::Disconnected);
}
void XrayProtocol::readXrayConfiguration(const QJsonObject &configuration)
ErrorCode XrayProtocol::startTun2Socks()
{
QJsonObject xrayConfiguration = configuration.value(ProtocolProps::key_proto_config_data(Proto::Xray)).toObject();
if (xrayConfiguration.isEmpty()) {
xrayConfiguration = configuration.value(ProtocolProps::key_proto_config_data(Proto::SSXray)).toObject();
m_tun2socksProcess = IpcClient::CreatePrivilegedProcess();
if (!m_tun2socksProcess->waitForSource()) {
return ErrorCode::AmneziaServiceConnectionFailed;
}
m_xrayConfig = xrayConfiguration;
m_routeMode = static_cast<Settings::RouteMode>(configuration.value(amnezia::config_key::splitTunnelType).toInt());
m_primaryDNS = configuration.value(amnezia::config_key::dns1).toString();
m_secondaryDNS = configuration.value(amnezia::config_key::dns2).toString();
m_tun2socksProcess->setProgram(PermittedProcess::Tun2Socks);
m_tun2socksProcess->setArguments({"-device", QString("tun://%1").arg(tunName), "-proxy", "socks5://127.0.0.1:10808" });
connect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardError, this, [this]() {
auto readAllStandardError = m_tun2socksProcess->readAllStandardError();
if (!readAllStandardError.waitForFinished()) {
qWarning() << "Failed to read output from tun2socks";
return;
}
const QString line = readAllStandardError.returnValue();
if (!line.contains("[TCP]") && !line.contains("[UDP]"))
qDebug() << "[tun2socks]:" << line;
if (line.contains("[STACK] tun://") && line.contains("<-> socks5://127.0.0.1")) {
disconnect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::readyReadStandardOutput, this, nullptr);
if (ErrorCode res = setupRouting(); res != ErrorCode::NoError) {
stop();
setLastError(res);
} else {
setConnectionState(Vpn::ConnectionState::Connected);
}
}
}, Qt::QueuedConnection);
connect(m_tun2socksProcess.data(), &IpcProcessInterfaceReplica::finished, this, [this](int exitCode, QProcess::ExitStatus exitStatus) {
if (exitStatus == QProcess::ExitStatus::CrashExit) {
qCritical() << "Tun2socks process crashed!";
} else {
qCritical() << QString("Tun2socks process was closed with %1 exit code").arg(exitCode);
}
stop();
setLastError(ErrorCode::Tun2SockExecutableCrashed);
}, Qt::QueuedConnection);
m_tun2socksProcess->start();
return ErrorCode::NoError;
}
ErrorCode XrayProtocol::setupRouting() {
return IpcClient::withInterface([this](QSharedPointer<IpcInterfaceReplica> iface) -> ErrorCode {
#ifdef Q_OS_WIN
const int inetAdapterIndex = NetworkUtilities::AdapterIndexTo(QHostAddress(m_remoteAddress));
#endif
auto createTun = iface->createTun(tunName, amnezia::protocols::xray::defaultLocalAddr);
if (!createTun.waitForFinished() || !createTun.returnValue()) {
qCritical() << "Failed to assign IP address for TUN";
return ErrorCode::InternalError;
}
auto updateResolvers = iface->updateResolvers(tunName, m_dnsServers);
if (!updateResolvers.waitForFinished() || !updateResolvers.returnValue()) {
qCritical() << "Failed to set DNS resolvers for TUN";
return ErrorCode::InternalError;
}
#ifdef Q_OS_WIN
int vpnAdapterIndex = -1;
QList<QNetworkInterface> netInterfaces = QNetworkInterface::allInterfaces();
for (auto& netInterface : netInterfaces) {
for (auto& address : netInterface.addressEntries()) {
if (m_vpnLocalAddress == address.ip().toString())
vpnAdapterIndex = netInterface.index();
}
}
#else
static const int vpnAdapterIndex = 0;
#endif
const bool killSwitchEnabled = QVariant(m_rawConfig.value(config_key::killSwitchOption).toString()).toBool();
if (killSwitchEnabled) {
if (vpnAdapterIndex != -1) {
QJsonObject config = m_rawConfig;
config.insert("vpnServer", m_remoteAddress);
auto enableKillSwitch = IpcClient::Interface()->enableKillSwitch(config, vpnAdapterIndex);
if (!enableKillSwitch.waitForFinished() || !enableKillSwitch.returnValue()) {
qCritical() << "Failed to enable killswitch";
return ErrorCode::InternalError;
}
} else
qWarning() << "Failed to get vpnAdapterIndex. Killswitch disabled";
}
if (m_routeMode == Settings::RouteMode::VpnAllSites) {
static const QStringList subnets = { "1.0.0.0/8", "2.0.0.0/7", "4.0.0.0/6", "8.0.0.0/5", "16.0.0.0/4", "32.0.0.0/3", "64.0.0.0/2", "128.0.0.0/1" };
auto routeAddList = iface->routeAddList(m_vpnGateway, subnets);
if (!routeAddList.waitForFinished() || routeAddList.returnValue() != subnets.count()) {
qCritical() << "Failed to set routes for TUN";
return ErrorCode::InternalError;
}
}
auto StopRoutingIpv6 = iface->StopRoutingIpv6();
if (!StopRoutingIpv6.waitForFinished() || !StopRoutingIpv6.returnValue()) {
qCritical() << "Failed to disable IPv6 routing";
return ErrorCode::InternalError;
}
#ifdef Q_OS_WIN
if (inetAdapterIndex != -1 && vpnAdapterIndex != -1) {
QJsonObject config = m_rawConfig;
config.insert("inetAdapterIndex", inetAdapterIndex);
config.insert("vpnAdapterIndex", vpnAdapterIndex);
config.insert("vpnGateway", m_vpnGateway);
config.insert("vpnServer", m_remoteAddress);
auto enablePeerTraffic = iface->enablePeerTraffic(config);
if (!enablePeerTraffic.waitForFinished() || !enablePeerTraffic.returnValue()) {
qCritical() << "Failed to enable peer traffic";
return ErrorCode::InternalError;
}
} else
qWarning() << "Failed to get adapter indexes. Split-tunneling disabled";
#endif
return ErrorCode::NoError;
},
[] () {
return ErrorCode::AmneziaServiceConnectionFailed;
});
}
+8 -8
View File
@@ -6,6 +6,7 @@
#include "core/ipcclient.h"
#include "vpnprotocol.h"
#include "settings.h"
#include <QtCore/qsharedpointer.h>
class XrayProtocol : public VpnProtocol
{
@@ -14,19 +15,18 @@ public:
virtual ~XrayProtocol() override;
ErrorCode start() override;
ErrorCode startTun2Sock();
void stop() override;
private:
void readXrayConfiguration(const QJsonObject &configuration);
ErrorCode setupRouting();
ErrorCode startTun2Socks();
QJsonObject m_xrayConfig;
Settings::RouteMode m_routeMode;
QString m_primaryDNS;
QString m_secondaryDNS;
#ifndef Q_OS_IOS
QSharedPointer<IpcProcessTun2SocksReplica> m_t2sProcess;
#endif
QList<QHostAddress> m_dnsServers;
QString m_remoteAddress;
QSharedPointer<IpcProcessInterfaceReplica> m_tun2socksProcess;
};
#endif // XRAYPROTOCOL_H
+1
View File
@@ -129,6 +129,7 @@
<file>ui/qml/Components/AdLabel.qml</file>
<file>ui/qml/Components/ConnectButton.qml</file>
<file>ui/qml/Components/ConnectionTypeSelectionDrawer.qml</file>
<file>ui/qml/Components/GamepadLoader.qml</file>
<file>ui/qml/Components/HomeContainersListView.qml</file>
<file>ui/qml/Components/HomeSplitTunnelingDrawer.qml</file>
<file>ui/qml/Components/InstalledAppsDrawer.qml</file>
+2 -1
View File
@@ -21,4 +21,5 @@ if [ "$(systemctl is-active docker)" != "active" ]; then \
sleep 5; sudo systemctl start docker; sleep 5;\
fi;\
if ! command -v sudo > /dev/null 2>&1; then echo "Failed to install sudo, command not found"; exit 1; fi;\
docker --version
docker --version;\
uname -sr
+1 -2
View File
@@ -14,8 +14,7 @@ namespace
const char cloudFlareNs1[] = "1.1.1.1";
const char cloudFlareNs2[] = "1.0.0.1";
//constexpr char gatewayEndpoint[] = "http://localhost:80/";
constexpr char gatewayEndpoint[] = "http://localhost:80/";
constexpr char gatewayEndpoint[] = "http://gw.amnezia.org:80/";
}
Settings::Settings(QObject *parent) : QObject(parent), m_settings(ORGANIZATION_NAME, APPLICATION_NAME, this)
+9
View File
@@ -94,6 +94,15 @@ public:
setValue("Conf/startMinimized", enabled);
}
bool isNewsNotifications() const
{
return value("Conf/newsNotifications", true).toBool();
}
void setNewsNotifications(bool enabled)
{
setValue("Conf/newsNotifications", enabled);
}
bool isSaveLogs() const
{
return value("Conf/saveLogs", false).toBool();
-81
View File
@@ -1,81 +0,0 @@
cmake_minimum_required(VERSION 3.25.0)
project(TransportTest)
set(CLIENT_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/..)
find_package(Qt6 REQUIRED COMPONENTS Core Network Test)
set(QSIMPLECRYPTO_DIR ${CLIENT_ROOT_DIR}/3rd/QSimpleCrypto/src)
set(OPENSSL_ROOT_DIR "${CLIENT_ROOT_DIR}/3rd-prebuilt/3rd-prebuilt/openssl/")
if(WIN32)
set(OPENSSL_INCLUDE_DIR "${OPENSSL_ROOT_DIR}/windows/include")
if("${CMAKE_SIZEOF_VOID_P}" STREQUAL "8")
set(OPENSSL_LIB_SSL "${OPENSSL_ROOT_DIR}/windows/win64/libssl.lib")
set(OPENSSL_LIB_CRYPTO "${OPENSSL_ROOT_DIR}/windows/win64/libcrypto.lib")
else()
set(OPENSSL_LIB_SSL "${OPENSSL_ROOT_DIR}/windows/win32/libssl.lib")
set(OPENSSL_LIB_CRYPTO "${OPENSSL_ROOT_DIR}/windows/win32/libcrypto.lib")
endif()
elseif(APPLE AND NOT IOS)
set(OPENSSL_INCLUDE_DIR "${OPENSSL_ROOT_DIR}/macos/include")
set(OPENSSL_LIB_SSL "${OPENSSL_ROOT_DIR}/macos/lib/libssl.a")
set(OPENSSL_LIB_CRYPTO "${OPENSSL_ROOT_DIR}/macos/lib/libcrypto.a")
elseif(LINUX)
set(OPENSSL_INCLUDE_DIR "${OPENSSL_ROOT_DIR}/linux/include")
set(OPENSSL_LIB_SSL "${OPENSSL_ROOT_DIR}/linux/x86_64/libssl.a")
set(OPENSSL_LIB_CRYPTO "${OPENSSL_ROOT_DIR}/linux/x86_64/libcrypto.a")
endif()
add_definitions(-DPROD_AGW_PUBLIC_KEY="$ENV{PROD_AGW_PUBLIC_KEY}")
add_definitions(-DDEV_AGW_PUBLIC_KEY="$ENV{DEV_AGW_PUBLIC_KEY}")
add_definitions(-DAGW_DNS_SERVER="$ENV{AGW_DNS_SERVER}")
add_definitions(-DAGW_DNS_DOMAIN="$ENV{AGW_DNS_DOMAIN}")
add_definitions(-DAGW_DNS_PRIMARY="$ENV{AGW_DNS_PRIMARY}")
add_definitions(-DAGW_DNS_PORT_UDP="$ENV{AGW_DNS_PORT_UDP}")
add_definitions(-DAGW_DNS_PORT_DOT="$ENV{AGW_DNS_PORT_DOT}")
add_definitions(-DAGW_DNS_PORT_DOH="$ENV{AGW_DNS_PORT_DOH}")
add_definitions(-DAGW_DNS_PORT_DOQ="$ENV{AGW_DNS_PORT_DOQ}")
add_definitions(-DAGW_DNS_DOH_PATH="$ENV{AGW_DNS_DOH_PATH}")
add_definitions(-DAGW_DNS_RETRY_COUNT="$ENV{AGW_DNS_RETRY_COUNT}")
add_definitions(-DAGW_DNS_TIMEOUT_MS="$ENV{AGW_DNS_TIMEOUT_MS}")
qt_add_executable(${PROJECT_NAME}
tst_transports.cpp
${CLIENT_ROOT_DIR}/core/transport/dns/dnsResolver.cpp
${CLIENT_ROOT_DIR}/core/transport/dns/dnsTunnel.cpp
${CLIENT_ROOT_DIR}/core/transport/dns/dnsPacket.cpp
${QSIMPLECRYPTO_DIR}/sources/QBlockCipher.cpp
${QSIMPLECRYPTO_DIR}/sources/QRsa.cpp
${QSIMPLECRYPTO_DIR}/sources/QX509.cpp
${QSIMPLECRYPTO_DIR}/sources/QX509Store.cpp
${QSIMPLECRYPTO_DIR}/sources/QAead.cpp
)
target_include_directories(${PROJECT_NAME} PRIVATE
${CLIENT_ROOT_DIR}
${CLIENT_ROOT_DIR}/core
${CLIENT_ROOT_DIR}/core/transport
${QSIMPLECRYPTO_DIR}
${QSIMPLECRYPTO_DIR}/include
${OPENSSL_INCLUDE_DIR}
)
target_compile_definitions(${PROJECT_NAME} PRIVATE
CLIENT_SOURCE_DIR="${CLIENT_ROOT_DIR}"
)
target_link_libraries(${PROJECT_NAME} PRIVATE
Qt6::Core
Qt6::Network
Qt6::Test
${OPENSSL_LIB_SSL}
${OPENSSL_LIB_CRYPTO}
)
if(WIN32)
target_link_libraries(${PROJECT_NAME} PRIVATE ws2_32 crypt32)
endif()
add_test(NAME TransportTest COMMAND ${PROJECT_NAME})
-406
View File
@@ -1,406 +0,0 @@
#include <QCoreApplication>
#include <QDebug>
#include <QElapsedTimer>
#include <QEventLoop>
#include <QHostAddress>
#include <QHostInfo>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QSslConfiguration>
#include <QSslError>
#include <QTest>
#include <QUrl>
#include "transport/dns/dnsResolver.h"
#include "transport/dns/dnsTunnel.h"
#include "QBlockCipher.h"
#include "QRsa.h"
#include <openssl/evp.h>
#include <openssl/rsa.h>
using amnezia::transport::dns::DnsProtocol;
struct TransportResult {
QString name;
bool success = false;
int elapsedMs = 0;
int responseSize = 0;
QString error;
QByteArray responseBody;
};
struct TestConfig {
QString httpEndpoint;
struct DnsEntry {
QString name;
DnsProtocol type;
QString server;
QString domain;
quint16 port;
QString dohPath;
};
QList<DnsEntry> dnsTransports;
int timeoutMs = 15000;
};
static TestConfig buildConfigFromEnv()
{
TestConfig cfg;
QString server(AGW_DNS_SERVER);
QString domain(AGW_DNS_DOMAIN);
cfg.httpEndpoint = QString(DEV_AGW_PUBLIC_KEY).isEmpty()
? QString() : QString("http://%1/").arg(server);
int timeout = QString(AGW_DNS_TIMEOUT_MS).toInt();
cfg.timeoutMs = (timeout > 0) ? timeout : 15000;
if (server.isEmpty() || domain.isEmpty()) return cfg;
auto addEntry = [&](DnsProtocol type, const QString &name,
const char *portDefine, quint16 defaultPort, const QString &dohPath = QString()) {
TestConfig::DnsEntry e;
e.type = type;
e.name = name;
e.server = server;
e.domain = domain;
quint16 port = QString(portDefine).toUShort();
e.port = (port > 0) ? port : defaultPort;
if (!dohPath.isEmpty()) e.dohPath = dohPath;
cfg.dnsTransports.append(e);
};
addEntry(DnsProtocol::Udp, "UDP", AGW_DNS_PORT_UDP, 5353);
addEntry(DnsProtocol::Tcp, "TCP", AGW_DNS_PORT_UDP, 5353);
addEntry(DnsProtocol::Tls, "DoT", AGW_DNS_PORT_DOT, 853);
QString dohPath = QString(AGW_DNS_DOH_PATH);
if (dohPath.isEmpty()) dohPath = "/dns-query";
addEntry(DnsProtocol::Https, "DoH", AGW_DNS_PORT_DOH, 443, dohPath);
addEntry(DnsProtocol::Quic, "DoQ", AGW_DNS_PORT_DOQ, 8853);
return cfg;
}
static QString resolveHost(const QString &host)
{
QHostAddress addr(host);
if (!addr.isNull()) return host;
QHostInfo info = QHostInfo::fromName(host);
if (!info.addresses().isEmpty())
return info.addresses().first().toString();
return host;
}
struct EncryptedPayload {
QByteArray body;
QByteArray key;
QByteArray iv;
QByteArray salt;
bool ok = false;
QString error;
};
static EncryptedPayload encryptPayload(const QJsonObject &apiPayload, const QByteArray &rsaPubKeyPem)
{
EncryptedPayload result;
QSimpleCrypto::QBlockCipher blockCipher;
result.key = blockCipher.generatePrivateSalt(32);
result.iv = blockCipher.generatePrivateSalt(32);
result.salt = blockCipher.generatePrivateSalt(8);
QJsonObject keyPayload;
keyPayload["aes_key"] = QString(result.key.toBase64());
keyPayload["aes_iv"] = QString(result.iv.toBase64());
keyPayload["aes_salt"] = QString(result.salt.toBase64());
try {
QSimpleCrypto::QRsa rsa;
QByteArray pemData = rsaPubKeyPem;
pemData.replace("\\n", "\n");
EVP_PKEY *pubKey = rsa.getPublicKeyFromByteArray(pemData);
if (!pubKey) {
result.error = "Failed to load RSA public key";
return result;
}
QByteArray encKeyPayload = rsa.encrypt(QJsonDocument(keyPayload).toJson(), pubKey, RSA_PKCS1_PADDING);
EVP_PKEY_free(pubKey);
QByteArray encApiPayload = blockCipher.encryptAesBlockCipher(
QJsonDocument(apiPayload).toJson(), result.key, result.iv, "", result.salt);
QJsonObject requestBody;
requestBody["key_payload"] = QString(encKeyPayload.toBase64());
requestBody["api_payload"] = QString(encApiPayload.toBase64());
result.body = QJsonDocument(requestBody).toJson();
result.ok = true;
} catch (const std::exception &ex) {
result.error = QString("Encryption failed: %1").arg(ex.what());
} catch (...) {
result.error = "Encryption failed: unknown error";
}
return result;
}
static QByteArray decryptResponse(const QByteArray &encrypted, const QByteArray &key,
const QByteArray &iv, const QByteArray &salt)
{
try {
QSimpleCrypto::QBlockCipher blockCipher;
return blockCipher.decryptAesBlockCipher(encrypted, key, iv, "", salt);
} catch (...) {
return QByteArray();
}
}
class TransportTest : public QObject
{
Q_OBJECT
private:
TestConfig m_config;
QByteArray m_rsaKey;
bool m_hasRsaKey = false;
QList<TransportResult> m_results;
void logResult(const TransportResult &r) {
QString status = r.success ? "OK" : "FAIL";
qDebug().noquote() << QString("[%1] %2 | %3ms | %4 bytes | %5")
.arg(status, -4)
.arg(r.name, -20)
.arg(r.elapsedMs, 5)
.arg(r.responseSize, 6)
.arg(r.error.isEmpty() ? "---" : r.error);
}
TransportResult doHttpTransport(const QString &endpoint, const QByteArray &payload) {
TransportResult r;
r.name = "HTTP";
QElapsedTimer timer;
timer.start();
QNetworkAccessManager nam;
QNetworkRequest request(QUrl(endpoint));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setTransferTimeout(m_config.timeoutMs);
QNetworkReply *reply = nam.post(request, payload);
QEventLoop loop;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
loop.exec();
r.elapsedMs = static_cast<int>(timer.elapsed());
if (reply->error() != QNetworkReply::NoError) {
r.error = QString("HTTP %1: %2")
.arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt())
.arg(reply->errorString());
r.responseBody = reply->readAll();
r.responseSize = r.responseBody.size();
} else {
r.responseBody = reply->readAll();
r.responseSize = r.responseBody.size();
r.success = !r.responseBody.isEmpty();
if (!r.success) r.error = "Empty response";
}
reply->deleteLater();
return r;
}
TransportResult doDnsTransport(const TestConfig::DnsEntry &entry, const QByteArray &payload,
const QString &resolvedIp) {
TransportResult r;
r.name = QString("DNS-%1").arg(entry.name);
QElapsedTimer timer;
timer.start();
bool needsHostname = (entry.type == DnsProtocol::Https || entry.type == DnsProtocol::Tls);
QString serverAddr = needsHostname ? entry.server : resolvedIp;
r.responseBody = amnezia::transport::dns::DnsTunnel::send(
payload, "services", entry.domain,
serverAddr, entry.type, entry.port,
m_config.timeoutMs, entry.dohPath);
r.elapsedMs = static_cast<int>(timer.elapsed());
r.responseSize = r.responseBody.size();
r.success = !r.responseBody.isEmpty();
if (!r.success) r.error = "Empty/no response";
return r;
}
private slots:
void initTestCase()
{
m_config = buildConfigFromEnv();
QVERIFY2(!m_config.dnsTransports.isEmpty(),
"AGW_DNS_SERVER / AGW_DNS_DOMAIN not set -- cannot run transport tests");
qDebug() << "HTTP endpoint:" << m_config.httpEndpoint;
qDebug() << "DNS transports:" << m_config.dnsTransports.size();
qDebug() << "Timeout:" << m_config.timeoutMs << "ms";
QByteArray prodKey(PROD_AGW_PUBLIC_KEY);
QByteArray devKey(DEV_AGW_PUBLIC_KEY);
if (!prodKey.isEmpty()) {
m_rsaKey = prodKey;
m_hasRsaKey = true;
qDebug() << "Using PROD_AGW_PUBLIC_KEY for E2E tests";
} else if (!devKey.isEmpty()) {
m_rsaKey = devKey;
m_hasRsaKey = true;
qDebug() << "Using DEV_AGW_PUBLIC_KEY for E2E tests";
} else {
qWarning() << "No RSA public key found -- E2E tests will be SKIPPED";
}
}
void test_transport_http()
{
QByteArray payload = R"({"test":true})";
TransportResult r = doHttpTransport(m_config.httpEndpoint, payload);
m_results.append(r);
logResult(r);
QVERIFY2(r.success || r.responseSize > 0,
qPrintable(QString("HTTP transport failed: %1").arg(r.error)));
}
void test_transport_dns_data()
{
QTest::addColumn<int>("transportIndex");
for (int i = 0; i < m_config.dnsTransports.size(); ++i) {
const auto &e = m_config.dnsTransports[i];
if (e.type == DnsProtocol::Quic) continue;
QTest::newRow(qPrintable(e.name)) << i;
}
}
void test_transport_dns()
{
QFETCH(int, transportIndex);
const auto &entry = m_config.dnsTransports[transportIndex];
QString resolvedIp = resolveHost(entry.server);
qDebug() << "Server:" << entry.server << "-> IP:" << resolvedIp
<< "Port:" << entry.port;
QByteArray payload = R"({"test":true})";
TransportResult r = doDnsTransport(entry, payload, resolvedIp);
m_results.append(r);
logResult(r);
if (!r.success) {
qWarning() << "DNS" << entry.name << "transport failed (server may be down):" << r.error;
}
}
void test_e2e_http()
{
if (!m_hasRsaKey) QSKIP("No RSA key -- skipping E2E");
QJsonObject apiPayload;
apiPayload["protocol"] = "any";
EncryptedPayload enc = encryptPayload(apiPayload, m_rsaKey);
QVERIFY2(enc.ok, qPrintable(enc.error));
TransportResult r = doHttpTransport(m_config.httpEndpoint, enc.body);
r.name = "E2E-HTTP";
if (r.success) {
QByteArray decrypted = decryptResponse(r.responseBody, enc.key, enc.iv, enc.salt);
if (!decrypted.isEmpty()) {
r.responseBody = decrypted;
r.responseSize = decrypted.size();
qDebug() << "Decrypted response:" << decrypted.left(200);
} else {
r.error = "Decryption failed (raw body size: " + QString::number(r.responseBody.size()) + ")";
r.success = false;
}
}
m_results.append(r);
logResult(r);
QVERIFY2(r.success, qPrintable(QString("E2E HTTP failed: %1").arg(r.error)));
}
void test_e2e_dns_data()
{
QTest::addColumn<int>("transportIndex");
for (int i = 0; i < m_config.dnsTransports.size(); ++i) {
const auto &e = m_config.dnsTransports[i];
if (e.type == DnsProtocol::Quic) continue;
QTest::newRow(qPrintable(QString("E2E-%1").arg(e.name))) << i;
}
}
void test_e2e_dns()
{
if (!m_hasRsaKey) QSKIP("No RSA key -- skipping E2E");
QFETCH(int, transportIndex);
const auto &entry = m_config.dnsTransports[transportIndex];
QString resolvedIp = resolveHost(entry.server);
qDebug() << "E2E via" << entry.name << "server:" << entry.server
<< "-> IP:" << resolvedIp << "port:" << entry.port;
QJsonObject apiPayload;
apiPayload["protocol"] = "any";
EncryptedPayload enc = encryptPayload(apiPayload, m_rsaKey);
QVERIFY2(enc.ok, qPrintable(enc.error));
TransportResult r = doDnsTransport(entry, enc.body, resolvedIp);
r.name = QString("E2E-%1").arg(entry.name);
if (r.success) {
QByteArray decrypted = decryptResponse(r.responseBody, enc.key, enc.iv, enc.salt);
if (!decrypted.isEmpty()) {
r.responseBody = decrypted;
r.responseSize = decrypted.size();
qDebug() << "Decrypted response:" << decrypted.left(200);
} else {
r.error = "Decryption failed (raw body size: " + QString::number(r.responseBody.size()) + ")";
r.success = false;
}
}
m_results.append(r);
logResult(r);
if (!r.success) {
qWarning() << "E2E DNS" << entry.name << "failed:" << r.error;
}
}
void cleanupTestCase()
{
qDebug() << "";
qDebug() << "============================================================";
qDebug() << " TRANSPORT TEST SUMMARY";
qDebug() << "============================================================";
qDebug().noquote() << QString(" %-4s | %-20s | %5s | %6s | %s")
.arg("", "Transport", "ms", "bytes", "Error");
qDebug() << "------------------------------------------------------------";
int passed = 0, failed = 0;
for (const auto &r : m_results) {
logResult(r);
if (r.success) ++passed; else ++failed;
}
qDebug() << "------------------------------------------------------------";
qDebug().noquote() << QString("Total: %1 passed, %2 failed, %3 total")
.arg(passed).arg(failed).arg(m_results.size());
qDebug() << "============================================================";
}
};
QTEST_MAIN(TransportTest)
#include "tst_transports.moc"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+56 -56
View File
@@ -199,7 +199,7 @@
<message>
<location filename="../ui/models/api/apiServicesModel.cpp" line="116"/>
<source>%1 $</source>
<translation>%1 $</translation>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/models/api/apiServicesModel.cpp" line="118"/>
@@ -672,32 +672,32 @@ Thank you for staying with us!</source>
<translation>Порт</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="422"/>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="418"/>
<source>Save</source>
<translation>Сохранить</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="431"/>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="427"/>
<source>Save settings?</source>
<translation>Сохранить настройки?</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="432"/>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="428"/>
<source>Only the settings for this device will be changed</source>
<translation>Будут изменены настройки только для этого устройства</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="433"/>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="429"/>
<source>Continue</source>
<translation>Продолжить</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="434"/>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="430"/>
<source>Cancel</source>
<translation>Отменить</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="438"/>
<location filename="../ui/qml/Pages2/PageProtocolAwgClientSettings.qml" line="434"/>
<source>Unable change settings while there is an active connection</source>
<translation>Невозможно изменить настройки во время активного соединения</translation>
</message>
@@ -1651,7 +1651,7 @@ Thank you for staying with us!</source>
<message>
<location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="204"/>
<source>mailto:support@amnezia.org</source>
<translation>mailto:support@amnezia.org</translation>
<translation></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsAbout.qml" line="211"/>
@@ -1775,72 +1775,72 @@ Thank you for staying with us!</source>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="22"/>
<source>Windows</source>
<translation>Windows</translation>
<translation></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="29"/>
<source>macOS</source>
<translation>macOS</translation>
<translation></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="36"/>
<source>Android</source>
<translation>Android</translation>
<translation></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="43"/>
<source>AndroidTV</source>
<translation>Android TV</translation>
<translation></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="50"/>
<source>iOS</source>
<translation>iOS</translation>
<translation></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="57"/>
<source>Linux</source>
<translation>Linux</translation>
<translation></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="64"/>
<source>Routers</source>
<translation>Маршрутизаторы</translation>
<translation></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="23"/>
<source>documentation/instructions/connect-amnezia-premium#windows</source>
<translation>documentation/instructions/connect-amnezia-premium#windows</translation>
<translation></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="30"/>
<source>documentation/instructions/connect-amnezia-premium#macos</source>
<translation>documentation/instructions/connect-amnezia-premium#macos</translation>
<translation></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="37"/>
<source>documentation/instructions/connect-amnezia-premium#android</source>
<translation>documentation/instructions/connect-amnezia-premium#android</translation>
<translation></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="44"/>
<source>documentation/instructions/android_tv_connect/</source>
<translation>documentation/instructions/android_tv_connect/</translation>
<translation></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="51"/>
<source>documentation/instructions/connect-amnezia-premium#ios</source>
<translation>documentation/instructions/connect-amnezia-premium#ios</translation>
<translation></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="58"/>
<source>documentation/instructions/connect-amnezia-premium#linux</source>
<translation>documentation/instructions/connect-amnezia-premium#linux</translation>
<translation></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="65"/>
<source>documentation/instructions/connect-amnezia-premium#routers</source>
<translation>documentation/instructions/connect-amnezia-premium#routers</translation>
<translation></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiInstructions.qml" line="101"/>
@@ -2111,7 +2111,7 @@ Thank you for staying with us!</source>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiSupport.qml" line="22"/>
<source>Telegram</source>
<translation>Telegram</translation>
<translation></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiSupport.qml" line="30"/>
@@ -2141,7 +2141,7 @@ Thank you for staying with us!</source>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiSupport.qml" line="110"/>
<source>Support tag</source>
<translation>Идентификатор поддержки</translation>
<translation></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiSupport.qml" line="120"/>
@@ -2272,12 +2272,12 @@ Thank you for staying with us!</source>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApplication.qml" line="180"/>
<source>News Notification</source>
<translation>Уведомления о новостях</translation>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApplication.qml" line="181"/>
<source>Show a notification icon for unread news</source>
<translation>Показывать значок уведомления, если есть непрочитанные новости</translation>
<source>Show notification icon when has unread news</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApplication.qml" line="221"/>
@@ -3115,17 +3115,17 @@ Thank you for staying with us!</source>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="113"/>
<source>Charged to your Apple ID at confirmation. Renews automatically unless auto-renew is turned off at least 24 hours before period end. Manage in Apple ID settings.</source>
<translation>Списание с Apple ID при подтверждении. Продление автоматическое, если автопродление не отключено минимум за 24 часа до окончания периода. Управление в настройках Apple ID.</translation>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="125"/>
<source>Subscribe Now</source>
<translation>Подписаться сейчас</translation>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="158"/>
<source>By continuing, you agree to the &lt;a href=&quot;%1&quot; style=&quot;color: #FBB26A;&quot;&gt;Terms of Use&lt;/a&gt; and &lt;a href=&quot;%2&quot; style=&quot;color: #FBB26A;&quot;&gt;Privacy Policy&lt;/a&gt;</source>
<translation>Продолжая, вы соглашаетесь с &lt;a href=&quot;%1&quot; style=&quot;color: #FBB26A;&quot;&gt;Условиями использования&lt;/a&gt; и &lt;a href=&quot;%2&quot; style=&quot;color: #FBB26A;&quot;&gt;Политикой конфиденциальности&lt;/a&gt;</translation>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml" line="186"/>
@@ -3697,7 +3697,7 @@ Thank you for staying with us!</source>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="270"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="572"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="566"/>
<source>Users</source>
<translation>Пользователи</translation>
</message>
@@ -3707,72 +3707,72 @@ Thank you for staying with us!</source>
<translation>Имя пользователя</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="588"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="582"/>
<source>Search</source>
<translation>Поиск</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="717"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="711"/>
<source>Creation date: %1</source>
<translation>Дата создания: %1</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="729"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="723"/>
<source>Latest handshake: %1</source>
<translation>Последнее рукопожатие: %1</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="741"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="735"/>
<source>Data received: %1</source>
<translation>Получено данных: %1</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="753"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="747"/>
<source>Data sent: %1</source>
<translation>Отправлено данных: %1</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="763"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="757"/>
<source>Allowed IPs: %1</source>
<translation>Разрешенные подсети: %1</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="778"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="772"/>
<source>Rename</source>
<translation>Переименовать</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="803"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="797"/>
<source>Client name</source>
<translation>Имя клиента</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="814"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="808"/>
<source>Save</source>
<translation>Сохранить</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="850"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="844"/>
<source>Revoke</source>
<translation>Отозвать</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="853"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="847"/>
<source>Revoke the config for a user - %1?</source>
<translation>Отозвать конфигурацию для пользователя - %1?</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="854"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="848"/>
<source>The user will no longer be able to connect to your server.</source>
<translation>Пользователь больше не сможет подключаться к вашему серверу.</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="855"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="849"/>
<source>Continue</source>
<translation>Продолжить</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="856"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="850"/>
<source>Cancel</source>
<translation>Отменить</translation>
</message>
@@ -3795,7 +3795,7 @@ Thank you for staying with us!</source>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="220"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="554"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="548"/>
<source>Share</source>
<translation>Поделиться</translation>
</message>
@@ -4241,7 +4241,7 @@ Thank you for staying with us!</source>
<message>
<location filename="../core/errorstrings.cpp" line="32"/>
<source>Server error: Linux kernel is too old</source>
<translation>Ошибка сервера: ядро Linux слишком старое</translation>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../core/errorstrings.cpp" line="35"/>
@@ -5020,47 +5020,47 @@ FileZilla или другие SFTP-клиенты, а также смонтир
<context>
<name>SitesController</name>
<message>
<location filename="../ui/controllers/sitesController.cpp" line="22"/>
<location filename="../ui/controllers/sitesController.cpp" line="24"/>
<source>Hostname not look like ip adress or domain name</source>
<translation>Имя хоста не похоже на IP-адрес или доменное имя</translation>
</message>
<message>
<location filename="../ui/controllers/sitesController.cpp" line="52"/>
<location filename="../ui/controllers/sitesController.cpp" line="66"/>
<source>New site added: %1</source>
<translation>Добавлен новый сайт: %1</translation>
</message>
<message>
<location filename="../ui/controllers/sitesController.cpp" line="61"/>
<location filename="../ui/controllers/sitesController.cpp" line="78"/>
<source>Site removed: %1</source>
<translation>Сайт удален: %1</translation>
</message>
<message>
<location filename="../ui/controllers/sitesController.cpp" line="68"/>
<location filename="../ui/controllers/sitesController.cpp" line="85"/>
<source>Site list cleared!</source>
<translation>Список сайтов очищен!</translation>
</message>
<message>
<location filename="../ui/controllers/sitesController.cpp" line="75"/>
<location filename="../ui/controllers/sitesController.cpp" line="92"/>
<source>Can&apos;t open file: %1</source>
<translation>Невозможно открыть файл: %1</translation>
</message>
<message>
<location filename="../ui/controllers/sitesController.cpp" line="81"/>
<location filename="../ui/controllers/sitesController.cpp" line="98"/>
<source>Failed to parse JSON data from file: %1</source>
<translation>Не удалось разобрать JSON-данные из файла: %1</translation>
</message>
<message>
<location filename="../ui/controllers/sitesController.cpp" line="86"/>
<location filename="../ui/controllers/sitesController.cpp" line="103"/>
<source>The JSON data is not an array in file: %1</source>
<translation>JSON-данные не являются массивом в файле: %1</translation>
</message>
<message>
<location filename="../ui/controllers/sitesController.cpp" line="114"/>
<location filename="../ui/controllers/sitesController.cpp" line="133"/>
<source>Import completed</source>
<translation>Импорт завершен</translation>
</message>
<message>
<location filename="../ui/controllers/sitesController.cpp" line="133"/>
<location filename="../ui/controllers/sitesController.cpp" line="152"/>
<source>Export completed</source>
<translation>Экспорт завершен</translation>
</message>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -5,7 +5,6 @@
#include "core/api/apiDefs.h"
#include "core/api/apiUtils.h"
#include "core/controllers/gatewayController.h"
#include "core/networkUtilities.h"
#include "core/qrCodeUtils.h"
#include "ui/controllers/systemController.h"
#include "version.h"
@@ -383,6 +382,51 @@ bool ApiConfigsController::fillAvailableServices()
}
QJsonObject data = QJsonDocument::fromJson(responseBody).object();
#if defined(Q_OS_IOS) || defined(MACOS_NE)
QEventLoop waitProducts;
bool productsFetched = false;
QString productPrice;
QString productCurrency;
IosController::Instance()->fetchProducts(QStringList() << QStringLiteral("amnezia_premium_6_month"),
[&](const QList<QVariantMap> &products,
const QStringList &invalidIds,
const QString &errorString) {
if (!errorString.isEmpty() || products.isEmpty()) {
qWarning().noquote() << "[IAP] Failed to fetch product price:" << errorString;
} else {
const auto &product = products.first();
productPrice = product.value("price").toString();
productCurrency = product.value("currencyCode").toString();
productsFetched = true;
qInfo().noquote() << "[IAP] Fetched product price:" << productPrice << productCurrency;
}
waitProducts.quit();
});
waitProducts.exec();
if (productsFetched && !productPrice.isEmpty()) {
QJsonArray services = data.value("services").toArray();
for (int i = 0; i < services.size(); ++i) {
QJsonObject service = services[i].toObject();
if (service.value(configKey::serviceType).toString() == serviceType::amneziaPremium) {
QJsonObject serviceInfo = service.value(configKey::serviceInfo).toObject();
QString formattedPrice = productPrice;
if (!productCurrency.isEmpty()) {
formattedPrice += " " + productCurrency;
}
serviceInfo["price"] = formattedPrice;
service[configKey::serviceInfo] = serviceInfo;
services[i] = service;
data["services"] = services;
qInfo().noquote() << "[IAP] Updated premium service price in data:" << formattedPrice;
break;
}
}
}
#endif
m_apiServicesModel->updateModel(data);
if (m_apiServicesModel->rowCount() > 0) {
m_apiServicesModel->setServiceIndex(0);
@@ -724,6 +768,9 @@ bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex)
QThread::msleep(10);
#endif
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
auto serverConfig = m_serversModel->getServerConfig(serverIndex);
auto installationUuid = m_settings->getInstallationUuid(true);
@@ -739,17 +786,7 @@ bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex)
apiPayload[configKey::apiEndpoint] = serverConfig.value(configKey::apiEndpoint).toString();
QByteArray responseBody;
QString endpoint = QString("%1v1/proxy_config");
// Use GatewayController with parallel transports
GatewayController gatewayController(m_settings->getGatewayEndpoint(),
m_settings->isDevGatewayEnv(),
apiDefs::requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
gatewayController.setTransportsConfig(GatewayController::buildTransportsConfig());
ErrorCode errorCode = gatewayController.post(endpoint, apiPayload, responseBody);
ErrorCode errorCode = gatewayController.post(QString("%1v1/proxy_config"), apiPayload, responseBody);
if (errorCode == ErrorCode::NoError) {
errorCode = fillServerConfig(serviceProtocol, protocolData, responseBody, serverConfig);
@@ -956,12 +993,7 @@ ErrorCode ApiConfigsController::importServiceFromBilling(const QByteArray &respo
ErrorCode ApiConfigsController::executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody,
bool isTestPurchase)
{
GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase),
m_settings->isDevGatewayEnv(isTestPurchase),
apiDefs::requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
gatewayController.setTransportsConfig(GatewayController::buildTransportsConfig());
GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase),
apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
return gatewayController.post(endpoint, apiPayload, responseBody);
}
+23 -32
View File
@@ -1,8 +1,6 @@
#include "apiNewsController.h"
#include "core/api/apiUtils.h"
#include "core/controllers/gatewayController.h"
#include "core/networkUtilities.h"
#include <QJsonDocument>
#include <QJsonObject>
@@ -33,6 +31,8 @@ void ApiNewsController::fetchNews(bool showError)
return;
}
auto gatewayController = QSharedPointer<GatewayController>::create(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(),
apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
QJsonObject payload;
payload.insert("locale", m_settings->getAppLanguage().name().split("_").first());
@@ -44,35 +44,26 @@ void ApiNewsController::fetchNews(bool showError)
payload.insert(configKey::serviceType, stacksJson.value(configKey::serviceType));
}
QString endpoint = QString("%1v1/news");
// Use GatewayController with parallel transports
GatewayController gatewayController(m_settings->getGatewayEndpoint(),
m_settings->isDevGatewayEnv(),
apiDefs::requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
gatewayController.setTransportsConfig(GatewayController::buildTransportsConfig());
QByteArray responseBody;
ErrorCode errorCode = gatewayController.post(endpoint, payload, responseBody);
if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode, showError);
return;
}
// Parse response
QJsonDocument doc = QJsonDocument::fromJson(responseBody);
QJsonArray newsArray;
if (doc.isArray()) {
newsArray = doc.array();
} else if (doc.isObject()) {
QJsonObject obj = doc.object();
if (obj.value("news").isArray()) {
newsArray = obj.value("news").toArray();
auto future = gatewayController->postAsync(QString("%1v1/news"), payload);
future.then(this, [this, showError, gatewayController](QPair<ErrorCode, QByteArray> result) {
auto [errorCode, responseBody] = result;
if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode, showError);
return;
}
}
m_newsModel->updateModel(newsArray);
emit fetchNewsFinished();
QJsonDocument doc = QJsonDocument::fromJson(responseBody);
QJsonArray newsArray;
if (doc.isArray()) {
newsArray = doc.array();
} else if (doc.isObject()) {
QJsonObject obj = doc.object();
if (obj.value("news").isArray()) {
newsArray = obj.value("news").toArray();
}
}
m_newsModel->updateModel(newsArray);
emit fetchNewsFinished();
});
}

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