mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-10-24 20:16:56 +09:00
Compare commits
121
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e6f029e18 | ||
|
|
4f8e1da751 | ||
|
|
eb1188ccd7 | ||
|
|
10289fdb26 | ||
|
|
511ce6f62a | ||
|
|
3a7d285e55 | ||
|
|
6123268846 | ||
|
|
49d111d1fe | ||
|
|
b591dd7445 | ||
|
|
a45bb5ea4f | ||
|
|
d859b111ca | ||
|
|
52031efc48 | ||
|
|
d78202c612 | ||
|
|
6bac948633 | ||
|
|
a4c4ef71fb | ||
|
|
127f85f4f0 | ||
|
|
13d4ddd292 | ||
|
|
7265e09c85 | ||
|
|
2e629b6dac | ||
|
|
92aba49705 | ||
|
|
bec06b3a5e | ||
|
|
91cd9474ea | ||
|
|
6178b05643 | ||
|
|
46ce22b85c | ||
|
|
36edafb985 | ||
|
|
d77eaba500 | ||
|
|
6a3d43fbb0 | ||
|
|
4975955bbe | ||
|
|
8f508783e3 | ||
|
|
f50817c43c | ||
|
|
54f67b3d82 | ||
|
|
d669adb707 | ||
|
|
5103bc640e | ||
|
|
3e6f0c0342 | ||
|
|
40950b92ee | ||
|
|
ac77b4ee75 | ||
|
|
fbf652f818 | ||
|
|
bbbf4891e6 | ||
|
|
20d005d66c | ||
|
|
c81ae2b060 | ||
|
|
105c42db1c | ||
|
|
89818ff63d | ||
|
|
414c422177 | ||
|
|
b39ac8556c | ||
|
|
5e1742262d | ||
|
|
5a07a1274f | ||
|
|
7b8ff1fd6e | ||
|
|
c7221832e0 | ||
|
|
eb7d031c7d | ||
|
|
3b3a0aaceb | ||
|
|
01ec79b7d5 | ||
|
|
3d6339e2dd | ||
|
|
b4d78d865a | ||
|
|
b53cdcff08 | ||
|
|
3cc18c5807 | ||
|
|
5fdce1e49e | ||
|
|
2ee61a040b | ||
|
|
741b5cc0f9 | ||
|
|
aaf0e070dc | ||
|
|
e0e126eda8 | ||
|
|
236daf6b3b | ||
|
|
f1481b1b1f | ||
|
|
f6e7d3ccf1 | ||
|
|
a754a11913 | ||
|
|
4d25e3b6f6 | ||
|
|
1fac280497 | ||
|
|
c886c5e6a7 | ||
|
|
cd7f78b9ca | ||
|
|
a587d3230f | ||
|
|
93e7b45136 | ||
|
|
e024f71ce1 | ||
|
|
50d1be7b4a | ||
|
|
3ec6d8973b | ||
|
|
3ea47d31a9 | ||
|
|
30c8cc4548 | ||
|
|
98586d2dd9 | ||
|
|
c66d8ecca0 | ||
|
|
db535f7e7d | ||
|
|
89f30d8c31 | ||
|
|
8bce432824 | ||
|
|
f3539b2632 | ||
|
|
7a96c212f3 | ||
|
|
2d5dc54e0f | ||
|
|
cef4c262e9 | ||
|
|
34309261a8 | ||
|
|
657eeb40c7 | ||
|
|
b4938c2cc9 | ||
|
|
524fefc5cb | ||
|
|
73f13404bb | ||
|
|
5fc68cca83 | ||
|
|
fcb7b8fa8d | ||
|
|
a81e32ff95 | ||
|
|
c897052107 | ||
|
|
4d0efc7ea5 | ||
|
|
a77842c9e3 | ||
|
|
0ded9db780 | ||
|
|
58d480fcb5 | ||
|
|
7154428d26 | ||
|
|
02a52d0169 | ||
|
|
ec60764072 | ||
|
|
17d2fa5532 | ||
|
|
3ca8b534e8 | ||
|
|
e88f7c5e46 | ||
|
|
3ac5d7bd1f | ||
|
|
19cad00a00 | ||
|
|
1ea716a163 | ||
|
|
4551659c2a | ||
|
|
c568bf8c24 | ||
|
|
a412d91105 | ||
|
|
ad01f23bbe | ||
|
|
656070b132 | ||
|
|
c907f5ca36 | ||
|
|
94a13b2b54 | ||
|
|
169f11d9c7 | ||
|
|
816dc3af95 | ||
|
|
b802863de5 | ||
|
|
8dc2a4b76c | ||
|
|
beb1c6dbf2 | ||
|
|
3eb06916c7 | ||
|
|
30d0f84a4f | ||
|
|
251f2aa5db |
No files matched your search
+141
-54
@@ -10,10 +10,10 @@ env:
|
||||
|
||||
jobs:
|
||||
Build-Linux-Ubuntu:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: 4-core
|
||||
|
||||
env:
|
||||
QT_VERSION: 6.6.2
|
||||
QT_VERSION: 6.8.3
|
||||
QIF_VERSION: 4.7
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||
@@ -30,13 +30,15 @@ jobs:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'linux'
|
||||
target: 'desktop'
|
||||
arch: 'gcc_64'
|
||||
arch: 'linux_gcc_64'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools'
|
||||
dir: ${{ runner.temp }}
|
||||
setup-python: 'true'
|
||||
tools: 'tools_ifw'
|
||||
set-env: 'true'
|
||||
extra: '--external 7z --base ${{ env.QT_MIRROR }}'
|
||||
aqtversion: '==3.3.0'
|
||||
py7zrversion: '==0.22.*'
|
||||
extra: '--base ${{ env.QT_MIRROR }}'
|
||||
|
||||
- name: 'Get sources'
|
||||
uses: actions/checkout@v4
|
||||
@@ -44,8 +46,15 @@ jobs:
|
||||
submodules: 'true'
|
||||
fetch-depth: 10
|
||||
|
||||
- name: 'Setup ccache'
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
- 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: |
|
||||
@@ -55,13 +64,13 @@ jobs:
|
||||
bash deploy/build_linux.sh
|
||||
|
||||
- name: 'Pack installer'
|
||||
run: cd deploy && tar -cf AmneziaVPN_Linux_Installer.tar AmneziaVPN_Linux_Installer.bin
|
||||
run: cd deploy && tar -cf AmneziaVPN_Linux_Installer.tar AmneziaVPN_Linux_Installer.bin && zip AmneziaVPN_${VERSION}_linux_x64.tar.zip AmneziaVPN_Linux_Installer.tar
|
||||
|
||||
- name: 'Upload installer artifact'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: AmneziaVPN_Linux_installer.tar
|
||||
path: deploy/AmneziaVPN_Linux_Installer.tar
|
||||
name: AmneziaVPN_${{ env.VERSION }}_linux_x64.tar.zip
|
||||
path: deploy/AmneziaVPN_${{ env.VERSION }}_linux_x64.tar.zip
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload unpacked artifact'
|
||||
@@ -84,7 +93,7 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
|
||||
env:
|
||||
QT_VERSION: 6.6.2
|
||||
QT_VERSION: 6.10.1
|
||||
QIF_VERSION: 4.7
|
||||
BUILD_ARCH: 64
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
@@ -102,8 +111,16 @@ jobs:
|
||||
submodules: 'true'
|
||||
fetch-depth: 10
|
||||
|
||||
- name: 'Setup ccache'
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
- 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
|
||||
@@ -111,32 +128,62 @@ jobs:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'windows'
|
||||
target: 'desktop'
|
||||
arch: 'win64_msvc2019_64'
|
||||
arch: 'win64_msvc2022_64'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools'
|
||||
dir: ${{ runner.temp }}
|
||||
setup-python: 'true'
|
||||
tools: 'tools_ifw'
|
||||
set-env: 'true'
|
||||
extra: '--external 7z --base ${{ env.QT_MIRROR }}'
|
||||
aqtversion: '==3.3.0'
|
||||
py7zrversion: '==0.22.*'
|
||||
extra: '--base ${{ env.QT_MIRROR }}'
|
||||
|
||||
- name: 'Setup mvsc'
|
||||
uses: ilammy/msvc-dev-cmd@v1
|
||||
with:
|
||||
arch: 'x64'
|
||||
|
||||
- name: 'Setup .NET SDK'
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: 'Install WiX Toolset'
|
||||
shell: powershell
|
||||
run: |
|
||||
dotnet tool install --global wix --version 4.0.6
|
||||
wix extension add -g WixToolset.UI.wixext/4.0.6
|
||||
wix extension add -g WixToolset.Util.wixext/4.0.6
|
||||
wix extension list -g
|
||||
$wixBinDir = Join-Path $env:USERPROFILE ".dotnet\tools"
|
||||
echo "WIX_BIN_DIR=$wixBinDir" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
|
||||
- name: 'Build project'
|
||||
shell: cmd
|
||||
run: |
|
||||
set BUILD_ARCH=${{ env.BUILD_ARCH }}
|
||||
set QT_BIN_DIR="${{ runner.temp }}\\Qt\\${{ env.QT_VERSION }}\\msvc2019_64\\bin"
|
||||
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
|
||||
|
||||
- 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
|
||||
with:
|
||||
name: AmneziaVPN_Windows_installer
|
||||
path: AmneziaVPN_x${{ env.BUILD_ARCH }}.exe
|
||||
name: AmneziaVPN_${{ env.VERSION }}_x64.exe
|
||||
path: AmneziaVPN_${{ env.VERSION }}_x64.exe
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload MSI installer artifact'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: AmneziaVPN_Windows_MSI_installer
|
||||
path: AmneziaVPN_x${{ env.BUILD_ARCH }}.msi
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload unpacked artifact'
|
||||
@@ -149,10 +196,10 @@ jobs:
|
||||
# ------------------------------------------------------
|
||||
|
||||
Build-iOS:
|
||||
runs-on: macos-13
|
||||
runs-on: macos-latest
|
||||
|
||||
env:
|
||||
QT_VERSION: 6.6.2
|
||||
QT_VERSION: 6.10.1
|
||||
CC: cc
|
||||
CXX: c++
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
@@ -167,7 +214,7 @@ jobs:
|
||||
- name: 'Setup xcode'
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: '15.2'
|
||||
xcode-version: '26.1'
|
||||
|
||||
- name: 'Install desktop Qt'
|
||||
uses: jurplel/install-qt-action@v3
|
||||
@@ -211,8 +258,8 @@ jobs:
|
||||
submodules: 'true'
|
||||
fetch-depth: 10
|
||||
|
||||
- name: 'Setup ccache'
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
# - name: 'Setup ccache'
|
||||
# uses: hendrikmuhs/ccache-action@v1.2
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: pip install jsonschema jinja2
|
||||
@@ -303,8 +350,8 @@ jobs:
|
||||
submodules: 'true'
|
||||
fetch-depth: 10
|
||||
|
||||
- name: 'Setup ccache'
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
# - name: 'Setup ccache'
|
||||
# uses: hendrikmuhs/ccache-action@v1.2
|
||||
|
||||
- name: 'Build project'
|
||||
run: |
|
||||
@@ -331,7 +378,7 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
|
||||
env:
|
||||
QT_VERSION: 6.8.0
|
||||
QT_VERSION: 6.10.1
|
||||
|
||||
MAC_TEAM_ID: ${{ secrets.MAC_TEAM_ID }}
|
||||
|
||||
@@ -361,7 +408,7 @@ jobs:
|
||||
xcode-version: '16.2.0'
|
||||
|
||||
- name: 'Install Qt'
|
||||
uses: jurplel/install-qt-action@v3
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'mac'
|
||||
@@ -371,8 +418,9 @@ jobs:
|
||||
dir: ${{ runner.temp }}
|
||||
setup-python: 'true'
|
||||
set-env: 'true'
|
||||
extra: '--external 7z --base ${{ env.QT_MIRROR }}'
|
||||
|
||||
aqtversion: '==3.3.0'
|
||||
py7zrversion: '==0.22.*'
|
||||
extra: '--base ${{ env.QT_MIRROR }}'
|
||||
|
||||
- name: 'Get sources'
|
||||
uses: actions/checkout@v4
|
||||
@@ -380,19 +428,32 @@ jobs:
|
||||
submodules: 'true'
|
||||
fetch-depth: 10
|
||||
|
||||
- name: 'Setup ccache'
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
- 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: |
|
||||
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 ../../..
|
||||
|
||||
- name: 'Upload installer artifact'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: AmneziaVPN_MacOS_installer
|
||||
path: deploy/build/pkg/AmneziaVPN.pkg
|
||||
name: AmneziaVPN_${{ env.VERSION }}_macos.zip
|
||||
path: deploy/AmneziaVPN_${{ env.VERSION }}_macos.zip
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload unpacked artifact'
|
||||
@@ -406,7 +467,7 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
|
||||
env:
|
||||
QT_VERSION: 6.8.3
|
||||
QT_VERSION: 6.10.1
|
||||
|
||||
MAC_TEAM_ID: ${{ secrets.MAC_TEAM_ID }}
|
||||
|
||||
@@ -426,21 +487,31 @@ jobs:
|
||||
- name: 'Setup xcode'
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: '16.2.0'
|
||||
xcode-version: '26.1'
|
||||
|
||||
- name: 'Install Qt'
|
||||
- name: 'Install desktop Qt'
|
||||
uses: jurplel/install-qt-action@v3
|
||||
with:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'mac'
|
||||
target: 'desktop'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qtmultimedia'
|
||||
arch: 'clang_64'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools'
|
||||
dir: ${{ runner.temp }}
|
||||
setup-python: 'true'
|
||||
set-env: 'true'
|
||||
extra: '--external 7z --base ${{ env.QT_MIRROR }}'
|
||||
extra: '--base ${{ env.QT_MIRROR }}'
|
||||
|
||||
- name: 'Install go'
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.24'
|
||||
cache: false
|
||||
|
||||
- name: 'Setup gomobile'
|
||||
run: |
|
||||
export PATH=$PATH:~/go/bin
|
||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||
gomobile init
|
||||
|
||||
- name: 'Get sources'
|
||||
uses: actions/checkout@v4
|
||||
@@ -448,8 +519,8 @@ jobs:
|
||||
submodules: 'true'
|
||||
fetch-depth: 10
|
||||
|
||||
- name: 'Setup ccache'
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
# - name: 'Setup ccache'
|
||||
# uses: hendrikmuhs/ccache-action@v1.2
|
||||
|
||||
- name: 'Build project'
|
||||
run: |
|
||||
@@ -466,11 +537,11 @@ jobs:
|
||||
# ------------------------------------------------------
|
||||
|
||||
Build-Android:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: 4-core
|
||||
|
||||
env:
|
||||
ANDROID_BUILD_PLATFORM: android-34
|
||||
QT_VERSION: 6.7.3
|
||||
ANDROID_BUILD_PLATFORM: android-36
|
||||
QT_VERSION: 6.10.1
|
||||
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||
@@ -551,15 +622,22 @@ jobs:
|
||||
with:
|
||||
submodules: 'true'
|
||||
|
||||
- name: 'Setup ccache'
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
- 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: 'Setup Java'
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
cache: 'gradle'
|
||||
# cache: 'gradle'
|
||||
|
||||
- name: 'Setup Android NDK'
|
||||
id: setup-ndk
|
||||
@@ -584,35 +662,44 @@ jobs:
|
||||
shell: bash
|
||||
run: ./deploy/build_android.sh --aab --apk all --build-platform ${{ env.ANDROID_BUILD_PLATFORM }}
|
||||
|
||||
- name: 'Rename Android APKs'
|
||||
run: |
|
||||
cd deploy/build
|
||||
mv AmneziaVPN-x86_64-release.apk AmneziaVPN_${VERSION}_android9+_x86_64.apk
|
||||
mv AmneziaVPN-x86-release.apk AmneziaVPN_${VERSION}_android9+_x86.apk
|
||||
mv AmneziaVPN-arm64-v8a-release.apk AmneziaVPN_${VERSION}_android9+_arm64-v8a.apk
|
||||
mv AmneziaVPN-armeabi-v7a-release.apk AmneziaVPN_${VERSION}_android9+_armeabi-v7a.apk
|
||||
cd ../..
|
||||
|
||||
- name: 'Upload x86_64 apk'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: AmneziaVPN-android-x86_64
|
||||
path: deploy/build/AmneziaVPN-x86_64-release.apk
|
||||
name: AmneziaVPN_${{ env.VERSION }}_android9+_x86_64.apk
|
||||
path: deploy/build/AmneziaVPN_${{ env.VERSION }}_android9+_x86_64.apk
|
||||
compression-level: 0
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload x86 apk'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: AmneziaVPN-android-x86
|
||||
path: deploy/build/AmneziaVPN-x86-release.apk
|
||||
name: AmneziaVPN_${{ env.VERSION }}_android9+_x86.apk
|
||||
path: deploy/build/AmneziaVPN_${{ env.VERSION }}_android9+_x86.apk
|
||||
compression-level: 0
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload arm64-v8a apk'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: AmneziaVPN-android-arm64-v8a
|
||||
path: deploy/build/AmneziaVPN-arm64-v8a-release.apk
|
||||
name: AmneziaVPN_${{ env.VERSION }}_android9+_arm64-v8a.apk
|
||||
path: deploy/build/AmneziaVPN_${{ env.VERSION }}_android9+_arm64-v8a.apk
|
||||
compression-level: 0
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload armeabi-v7a apk'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: AmneziaVPN-android-armeabi-v7a
|
||||
path: deploy/build/AmneziaVPN-armeabi-v7a-release.apk
|
||||
name: AmneziaVPN_${{ env.VERSION }}_android9+_armeabi-v7a.apk
|
||||
path: deploy/build/AmneziaVPN_${{ env.VERSION }}_android9+_armeabi-v7a.apk
|
||||
compression-level: 0
|
||||
retention-days: 7
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# User settings
|
||||
*.user
|
||||
|
||||
# Gateway configs (contains sensitive endpoints)
|
||||
gateway.json
|
||||
client/gateway.json
|
||||
macOSPackage/
|
||||
AmneziaVPN.dmg
|
||||
AmneziaVPN.exe
|
||||
@@ -9,6 +13,7 @@ deploy/build_32/*
|
||||
deploy/build_64/*
|
||||
winbuild*.bat
|
||||
.cache/
|
||||
.vscode/
|
||||
|
||||
|
||||
# Qt-es
|
||||
|
||||
+35
-2
@@ -1,7 +1,7 @@
|
||||
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
|
||||
|
||||
set(PROJECT AmneziaVPN)
|
||||
set(AMNEZIAVPN_VERSION 4.8.9.2)
|
||||
set(AMNEZIAVPN_VERSION 4.8.12.8)
|
||||
|
||||
project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
|
||||
DESCRIPTION "AmneziaVPN"
|
||||
@@ -12,7 +12,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 2092)
|
||||
set(APP_ANDROID_VERSION_CODE 2104)
|
||||
|
||||
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||
set(MZ_PLATFORM_NAME "linux")
|
||||
@@ -49,3 +49,36 @@ if(NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
|
||||
|
||||
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)
|
||||
endif()
|
||||
+1
-1
Submodule client/3rd-prebuilt updated: 02a00ee00d...51bb4703a4.
Vendored
+1
-1
Submodule client/3rd/amneziawg-apple updated: 811af0a83b...cf63135331.
+22
-1
@@ -33,6 +33,21 @@ 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))
|
||||
set(PACKAGES ${PACKAGES} Widgets)
|
||||
endif()
|
||||
@@ -56,7 +71,7 @@ target_include_directories(${PROJECT} PUBLIC
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>
|
||||
)
|
||||
|
||||
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
|
||||
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
|
||||
qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_interface.rep)
|
||||
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)
|
||||
@@ -229,3 +244,9 @@ 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)
|
||||
endif()
|
||||
@@ -13,6 +13,8 @@
|
||||
#include <QTimer>
|
||||
#include <QTranslator>
|
||||
#include <QEvent>
|
||||
#include <QDir>
|
||||
#include <QSettings>
|
||||
|
||||
#include "logger.h"
|
||||
#include "ui/controllers/pageController.h"
|
||||
@@ -25,8 +27,15 @@
|
||||
#include <QtQuick/QQuickWindow> // for QQuickWindow
|
||||
#include <QWindow> // for qobject_cast<QWindow*>
|
||||
|
||||
AmneziaApplication::AmneziaApplication(int &argc, char *argv[]) : AMNEZIA_BASE_CLASS(argc, argv)
|
||||
bool AmneziaApplication::m_forceQuit = false;
|
||||
|
||||
AmneziaApplication::AmneziaApplication(int &argc, char *argv[]) : AMNEZIA_BASE_CLASS(argc, argv),
|
||||
m_optAutostart({QStringLiteral("a"), QStringLiteral("autostart")}, QStringLiteral("System autostart")),
|
||||
m_optCleanup ({QStringLiteral("c"), QStringLiteral("cleanup")}, QStringLiteral("Cleanup logs")),
|
||||
m_optConnect ({QStringLiteral("connect")}, QStringLiteral("Connect to server by index on startup"), QStringLiteral("index")),
|
||||
m_optImport ({QStringLiteral("import")}, QStringLiteral("Import configuration from data string"), QStringLiteral("data"))
|
||||
{
|
||||
setDesktopFileName(QStringLiteral(APPLICATION_NAME));
|
||||
setQuitOnLastWindowClosed(false);
|
||||
|
||||
// Fix config file permissions
|
||||
@@ -51,11 +60,13 @@ AmneziaApplication::AmneziaApplication(int &argc, char *argv[]) : AMNEZIA_BASE_C
|
||||
|
||||
AmneziaApplication::~AmneziaApplication()
|
||||
{
|
||||
if (m_vpnConnection) {
|
||||
QMetaObject::invokeMethod(m_vpnConnection.get(), "disconnectFromVpn", Qt::QueuedConnection);
|
||||
QThread::msleep(2000);
|
||||
QMetaObject::invokeMethod(m_vpnConnection.get(), "disconnectSlots", Qt::QueuedConnection);
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
if (m_vpnConnection && m_vpnConnectionThread.isRunning()) {
|
||||
QMetaObject::invokeMethod(m_vpnConnection.get(), "disconnectSlots", Qt::BlockingQueuedConnection);
|
||||
|
||||
QMetaObject::invokeMethod(m_vpnConnection.get(), "disconnectFromVpn", Qt::BlockingQueuedConnection);
|
||||
}
|
||||
#endif
|
||||
|
||||
m_vpnConnectionThread.requestInterruption();
|
||||
m_vpnConnectionThread.quit();
|
||||
@@ -66,11 +77,23 @@ AmneziaApplication::~AmneziaApplication()
|
||||
}
|
||||
|
||||
if (m_engine) {
|
||||
QObject::disconnect(m_engine, 0, 0, 0);
|
||||
delete m_engine;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
namespace {
|
||||
static void clearQtCaches()
|
||||
{
|
||||
const QString cacheRoot = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
|
||||
if (!cacheRoot.isEmpty()) {
|
||||
QDir(cacheRoot + "/QtShaderCache").removeRecursively();
|
||||
QDir(cacheRoot + "/qmlcache").removeRecursively();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void AmneziaApplication::init()
|
||||
{
|
||||
m_engine = new QQmlApplicationEngine;
|
||||
@@ -106,6 +129,16 @@ void AmneziaApplication::init()
|
||||
m_coreController.reset(new CoreController(m_vpnConnection, m_settings, m_engine));
|
||||
|
||||
m_engine->addImportPath("qrc:/ui/qml/Modules/");
|
||||
|
||||
if (m_parser.isSet(m_optImport)) {
|
||||
const QString data = m_parser.value(m_optImport);
|
||||
if (!data.isEmpty()) {
|
||||
if (m_coreController) {
|
||||
m_coreController->importConfigFromData(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_engine->load(url);
|
||||
|
||||
m_coreController->setQmlRoot();
|
||||
@@ -121,7 +154,7 @@ void AmneziaApplication::init()
|
||||
Logger::setServiceLogsEnabled(enabled);
|
||||
|
||||
#ifdef Q_OS_WIN //TODO
|
||||
if (m_parser.isSet("a"))
|
||||
if (m_parser.isSet(m_optAutostart))
|
||||
m_coreController->pageController()->showOnStartup();
|
||||
else
|
||||
emit m_coreController->pageController()->raiseMainWindow();
|
||||
@@ -145,6 +178,18 @@ void AmneziaApplication::init()
|
||||
}
|
||||
});
|
||||
#endif
|
||||
|
||||
if (m_parser.isSet(m_optConnect)) {
|
||||
bool ok = false;
|
||||
int idx = m_parser.value(m_optConnect).toInt(&ok);
|
||||
if (ok) {
|
||||
QTimer::singleShot(0, this, [this, idx]() {
|
||||
if (m_coreController) {
|
||||
m_coreController->openConnectionByIndex(idx);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AmneziaApplication::registerTypes()
|
||||
@@ -189,15 +234,14 @@ bool AmneziaApplication::parseCommands()
|
||||
m_parser.addHelpOption();
|
||||
m_parser.addVersionOption();
|
||||
|
||||
QCommandLineOption c_autostart { { "a", "autostart" }, "System autostart" };
|
||||
m_parser.addOption(c_autostart);
|
||||
|
||||
QCommandLineOption c_cleanup { { "c", "cleanup" }, "Cleanup logs" };
|
||||
m_parser.addOption(c_cleanup);
|
||||
m_parser.addOption(m_optAutostart);
|
||||
m_parser.addOption(m_optCleanup);
|
||||
m_parser.addOption(m_optConnect);
|
||||
m_parser.addOption(m_optImport);
|
||||
|
||||
m_parser.process(*this);
|
||||
|
||||
if (m_parser.isSet(c_cleanup)) {
|
||||
if (m_parser.isSet(m_optCleanup)) {
|
||||
Logger::cleanUp();
|
||||
QTimer::singleShot(100, this, [this] { quit(); });
|
||||
exec();
|
||||
@@ -230,8 +274,12 @@ bool AmneziaApplication::eventFilter(QObject *watched, QEvent *event)
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||
quit();
|
||||
#else
|
||||
if (m_coreController && m_coreController->pageController()) {
|
||||
m_coreController->pageController()->hideMainWindow();
|
||||
if (m_forceQuit) {
|
||||
quit();
|
||||
} else {
|
||||
if (m_coreController && m_coreController->pageController()) {
|
||||
m_coreController->pageController()->hideMainWindow();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return true; // eat the close
|
||||
@@ -240,6 +288,12 @@ bool AmneziaApplication::eventFilter(QObject *watched, QEvent *event)
|
||||
return QObject::eventFilter(watched, event);
|
||||
}
|
||||
|
||||
void AmneziaApplication::forceQuit()
|
||||
{
|
||||
m_forceQuit = true;
|
||||
quit();
|
||||
}
|
||||
|
||||
QQmlApplicationEngine *AmneziaApplication::qmlEngine() const
|
||||
{
|
||||
return m_engine;
|
||||
|
||||
@@ -45,7 +45,11 @@ public:
|
||||
QNetworkAccessManager *networkManager();
|
||||
QClipboard *getClipboard();
|
||||
|
||||
public slots:
|
||||
void forceQuit();
|
||||
|
||||
private:
|
||||
static bool m_forceQuit;
|
||||
QQmlApplicationEngine *m_engine {};
|
||||
std::shared_ptr<Settings> m_settings;
|
||||
|
||||
@@ -56,6 +60,11 @@ private:
|
||||
|
||||
QCommandLineParser m_parser;
|
||||
|
||||
QCommandLineOption m_optAutostart;
|
||||
QCommandLineOption m_optCleanup;
|
||||
QCommandLineOption m_optConnect;
|
||||
QCommandLineOption m_optImport;
|
||||
|
||||
QSharedPointer<VpnConnection> m_vpnConnection;
|
||||
QThread m_vpnConnectionThread;
|
||||
|
||||
|
||||
@@ -45,7 +45,8 @@
|
||||
android:configChanges="uiMode|screenSize|smallestScreenSize|screenLayout|orientation|density
|
||||
|fontScale|layoutDirection|locale|keyboard|keyboardHidden|navigation|mcc|mnc"
|
||||
android:launchMode="singleInstance"
|
||||
android:windowSoftInputMode="stateUnchanged|adjustResize"
|
||||
android:windowSoftInputMode="adjustResize|stateUnchanged"
|
||||
android:enableOnBackInvokedCallback="false"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
@@ -214,4 +215,4 @@
|
||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/qtprovider_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
||||
</manifest>
|
||||
@@ -93,7 +93,7 @@ open class OpenVpn : Protocol() {
|
||||
openVpnClient = null
|
||||
}
|
||||
|
||||
override fun reconnectVpn(vpnBuilder: Builder) {
|
||||
override fun reconnectVpn(vpnBuilder: Builder, protect: (Int) -> Boolean) {
|
||||
openVpnClient?.let {
|
||||
it.establish = makeEstablish(vpnBuilder)
|
||||
it.reconnect(0)
|
||||
|
||||
@@ -42,7 +42,7 @@ abstract class Protocol {
|
||||
|
||||
abstract fun stopVpn()
|
||||
|
||||
abstract fun reconnectVpn(vpnBuilder: Builder)
|
||||
abstract fun reconnectVpn(vpnBuilder: Builder, protect: (Int) -> Boolean)
|
||||
|
||||
protected fun ProtocolConfig.Builder.configSplitTunneling(config: JSONObject) {
|
||||
if (!allowSplitTunneling) {
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
<item name="android:colorBackground">@color/black</item>
|
||||
<item name="android:windowActionBar">false</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
<item name="android:enforceNavigationBarContrast">false</item>
|
||||
<item name="android:enforceStatusBarContrast">false</item>
|
||||
</style>
|
||||
<style name="Translucent" parent="NoActionBar">
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
|
||||
@@ -35,6 +35,11 @@ import android.widget.Toast
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.OnApplyWindowInsetsListener
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import java.io.IOException
|
||||
import kotlin.LazyThreadSafetyMode.NONE
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
@@ -170,10 +175,9 @@ class AmneziaActivity : QtActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
Log.d(TAG, "Create Amnezia activity")
|
||||
loadLibs()
|
||||
window.apply {
|
||||
addFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
|
||||
statusBarColor = getColor(R.color.black)
|
||||
}
|
||||
|
||||
// Configure window for edge-to-edge display
|
||||
configureWindowForEdgeToEdge()
|
||||
mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||
val proto = mainScope.async(Dispatchers.IO) {
|
||||
VpnStateStore.getVpnState().vpnProto
|
||||
@@ -265,6 +269,98 @@ class AmneziaActivity : QtActivity() {
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
super.onWindowFocusChanged(hasFocus)
|
||||
Log.d(TAG, "Window focus changed: hasFocus=$hasFocus")
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
Log.d(TAG, "Pause Amnezia activity")
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
/* if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
window.decorView.apply {
|
||||
invalidate()
|
||||
|
||||
postDelayed({
|
||||
sendTouch(1f, 1f)
|
||||
}, 100)
|
||||
|
||||
postDelayed({
|
||||
sendTouch(2f, 2f)
|
||||
}, 200)
|
||||
|
||||
postDelayed({
|
||||
requestLayout()
|
||||
invalidate()
|
||||
}, 250)
|
||||
}
|
||||
} */
|
||||
Log.d(TAG, "Resume Amnezia activity")
|
||||
}
|
||||
|
||||
private fun configureWindowForEdgeToEdge() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
window.apply {
|
||||
addFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
|
||||
addFlags(LayoutParams.FLAG_LAYOUT_NO_LIMITS)
|
||||
statusBarColor = android.graphics.Color.TRANSPARENT
|
||||
navigationBarColor = android.graphics.Color.TRANSPARENT
|
||||
}
|
||||
|
||||
WindowInsetsControllerCompat(window, window.decorView).apply {
|
||||
isAppearanceLightStatusBars = false
|
||||
isAppearanceLightNavigationBars = false
|
||||
}
|
||||
|
||||
// Workaround for Android 14 (API 34+) IME adjustResize bug
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
setupImeInsetsListener()
|
||||
}
|
||||
} else {
|
||||
window.apply {
|
||||
addFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
|
||||
statusBarColor = getColor(R.color.black)
|
||||
}
|
||||
|
||||
WindowInsetsControllerCompat(window, window.decorView).apply {
|
||||
isAppearanceLightStatusBars = false
|
||||
isAppearanceLightNavigationBars = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupImeInsetsListener() {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { view, windowInsets ->
|
||||
val imeInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime())
|
||||
val imeVisible = windowInsets.isVisible(WindowInsetsCompat.Type.ime())
|
||||
|
||||
val imeHeight = if (imeVisible) imeInsets.bottom else 0
|
||||
|
||||
val density = resources.displayMetrics.density
|
||||
val imeHeightDp = (imeHeight / density).toInt()
|
||||
|
||||
// Also track system bars (navigation bar, status bar) changes
|
||||
val systemBarsInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
val navBarHeight = systemBarsInsets.bottom
|
||||
val navBarHeightDp = (navBarHeight / density).toInt()
|
||||
val statusBarHeight = systemBarsInsets.top
|
||||
val statusBarHeightDp = (statusBarHeight / density).toInt()
|
||||
|
||||
mainScope.launch {
|
||||
qtInitialized.await()
|
||||
QtAndroidController.onImeInsetsChanged(imeHeightDp)
|
||||
QtAndroidController.onSystemBarsInsetsChanged(navBarHeightDp, statusBarHeightDp)
|
||||
}
|
||||
|
||||
// Return windowInsets instead of CONSUMED to allow proper handling
|
||||
windowInsets
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Log.d(TAG, "Destroy Amnezia activity")
|
||||
unregisterBroadcastReceiver(notificationStateReceiver)
|
||||
@@ -666,6 +762,43 @@ class AmneziaActivity : QtActivity() {
|
||||
@Suppress("unused")
|
||||
fun isOnTv(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||
|
||||
@Suppress("unused")
|
||||
fun isEdgeToEdgeEnabled(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
|
||||
|
||||
@Suppress("unused")
|
||||
fun getStatusBarHeight(): Int {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) return 0
|
||||
|
||||
val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android")
|
||||
val heightPx = if (resourceId > 0) {
|
||||
resources.getDimensionPixelSize(resourceId)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
// Convert physical pixels to device-independent pixels for QML
|
||||
val density = resources.displayMetrics.density
|
||||
val heightDp = (heightPx / density).toInt()
|
||||
return heightDp
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun getNavigationBarHeight(): Int {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) return 0
|
||||
|
||||
val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")
|
||||
val heightPx = if (resourceId > 0) {
|
||||
resources.getDimensionPixelSize(resourceId)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
// Convert physical pixels to device-independent pixels for QML
|
||||
val density = resources.displayMetrics.density
|
||||
val heightDp = (heightPx / density).toInt()
|
||||
return heightDp
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun startQrCodeReader() {
|
||||
Log.v(TAG, "Start camera")
|
||||
|
||||
@@ -565,7 +565,7 @@ open class AmneziaVpnService : VpnService() {
|
||||
protocolState.value = RECONNECTING
|
||||
|
||||
connectionJob = connectionScope.launch {
|
||||
vpnProto?.protocol?.reconnectVpn(Builder())
|
||||
vpnProto?.protocol?.reconnectVpn(Builder(), ::protect)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,15 +38,15 @@ object AppListProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private class App(pi: PackageInfo, pm: PackageManager, ai: ApplicationInfo = pi.applicationInfo) : Comparable<App> {
|
||||
private class App(pi: PackageInfo, pm: PackageManager, ai: ApplicationInfo? = pi.applicationInfo) : Comparable<App> {
|
||||
val name: String?
|
||||
val packageName: String = pi.packageName
|
||||
val icon: Boolean = ai.icon != 0
|
||||
val icon: Boolean = (ai?.icon ?: 0) != 0
|
||||
val isLaunchable: Boolean = pm.getLaunchIntentForPackage(packageName) != null
|
||||
|
||||
init {
|
||||
val name = ai.loadLabel(pm).toString()
|
||||
this.name = if (name != packageName) name else null
|
||||
val name = ai?.loadLabel(pm)?.toString()
|
||||
this.name = name?.takeIf { it != packageName }
|
||||
}
|
||||
|
||||
override fun compareTo(other: App): Int {
|
||||
|
||||
@@ -28,4 +28,7 @@ object QtAndroidController {
|
||||
external fun onAuthResult(result: Boolean)
|
||||
|
||||
external fun decodeQrCode(data: String): Boolean
|
||||
|
||||
external fun onImeInsetsChanged(heightDp: Int)
|
||||
external fun onSystemBarsInsetsChanged(navBarHeightDp: Int, statusBarHeightDp: Int)
|
||||
}
|
||||
+29
-17
@@ -12,6 +12,7 @@ import org.amnezia.vpn.protocol.Protocol
|
||||
import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
|
||||
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
|
||||
import org.amnezia.vpn.protocol.Statistics
|
||||
import org.amnezia.vpn.protocol.VpnException
|
||||
import org.amnezia.vpn.protocol.VpnStartException
|
||||
import org.amnezia.vpn.util.LibraryLoader.loadSharedLibrary
|
||||
import org.amnezia.vpn.util.Log
|
||||
@@ -27,6 +28,7 @@ private const val TAG = "Wireguard"
|
||||
open class Wireguard : Protocol() {
|
||||
|
||||
private var tunnelHandle: Int = -1
|
||||
private var config: WireguardConfig? = null // save config for reconnect
|
||||
protected open val ifName: String = "amn0"
|
||||
private lateinit var scope: CoroutineScope
|
||||
private var statusJob: Job? = null
|
||||
@@ -61,6 +63,7 @@ open class Wireguard : Protocol() {
|
||||
override suspend fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) {
|
||||
val wireguardConfig = parseConfig(config)
|
||||
start(wireguardConfig, vpnBuilder, protect)
|
||||
this.config = wireguardConfig
|
||||
}
|
||||
|
||||
protected open fun parseConfig(config: JSONObject): WireguardConfig {
|
||||
@@ -122,23 +125,24 @@ open class Wireguard : Protocol() {
|
||||
configData.optStringOrNull("S2")?.let { setS2(it.toInt()) }
|
||||
configData.optStringOrNull("S3")?.let { setS3(it.toInt()) }
|
||||
configData.optStringOrNull("S4")?.let { setS4(it.toInt()) }
|
||||
configData.optStringOrNull("H1")?.let { setH1(it.toLong()) }
|
||||
configData.optStringOrNull("H2")?.let { setH2(it.toLong()) }
|
||||
configData.optStringOrNull("H3")?.let { setH3(it.toLong()) }
|
||||
configData.optStringOrNull("H4")?.let { setH4(it.toLong()) }
|
||||
configData.optStringOrNull("H1")?.trim()?.let { if (it.isNotEmpty()) setH1(it) }
|
||||
configData.optStringOrNull("H2")?.trim()?.let { if (it.isNotEmpty()) setH2(it) }
|
||||
configData.optStringOrNull("H3")?.trim()?.let { if (it.isNotEmpty()) setH3(it) }
|
||||
configData.optStringOrNull("H4")?.trim()?.let { if (it.isNotEmpty()) setH4(it) }
|
||||
configData.optStringOrNull("I1")?.let { setI1(it) }
|
||||
configData.optStringOrNull("I2")?.let { setI2(it) }
|
||||
configData.optStringOrNull("I3")?.let { setI3(it) }
|
||||
configData.optStringOrNull("I4")?.let { setI4(it) }
|
||||
configData.optStringOrNull("I5")?.let { setI5(it) }
|
||||
configData.optStringOrNull("J1")?.let { setJ1(it) }
|
||||
configData.optStringOrNull("J2")?.let { setJ2(it) }
|
||||
configData.optStringOrNull("J3")?.let { setJ3(it) }
|
||||
configData.optStringOrNull("Itime")?.let { setItime(it.toInt()) }
|
||||
}
|
||||
|
||||
private fun start(config: WireguardConfig, vpnBuilder: Builder, protect: (Int) -> Boolean) {
|
||||
if (tunnelHandle != -1) {
|
||||
private fun start(
|
||||
config: WireguardConfig,
|
||||
vpnBuilder: Builder,
|
||||
protect: (Int) -> Boolean,
|
||||
stopExistingVpn: Boolean = false
|
||||
) {
|
||||
if (!stopExistingVpn && tunnelHandle != -1) {
|
||||
Log.w(TAG, "Tunnel already up")
|
||||
return
|
||||
}
|
||||
@@ -146,6 +150,9 @@ open class Wireguard : Protocol() {
|
||||
buildVpnInterface(config, vpnBuilder)
|
||||
|
||||
vpnBuilder.establish().use { tunFd ->
|
||||
if (stopExistingVpn && tunnelHandle != -1) {
|
||||
turnOffVpn()
|
||||
}
|
||||
if (tunFd == null) {
|
||||
throw VpnStartException("Create VPN interface: permission not granted or revoked")
|
||||
}
|
||||
@@ -202,20 +209,25 @@ open class Wireguard : Protocol() {
|
||||
return lastHandshake
|
||||
}
|
||||
|
||||
override fun stopVpn() {
|
||||
if (tunnelHandle == -1) {
|
||||
Log.w(TAG, "Tunnel already down")
|
||||
return
|
||||
}
|
||||
private fun turnOffVpn() {
|
||||
statusJob?.cancel()
|
||||
statusJob = null
|
||||
val handleToClose = tunnelHandle
|
||||
tunnelHandle = -1
|
||||
GoBackend.awgTurnOff(handleToClose)
|
||||
}
|
||||
|
||||
override fun stopVpn() {
|
||||
if (tunnelHandle == -1) {
|
||||
Log.w(TAG, "Tunnel already down")
|
||||
return
|
||||
}
|
||||
turnOffVpn()
|
||||
state.value = DISCONNECTED
|
||||
}
|
||||
|
||||
override fun reconnectVpn(vpnBuilder: Builder) {
|
||||
state.value = CONNECTED
|
||||
override fun reconnectVpn(vpnBuilder: Builder, protect: (Int) -> Boolean) {
|
||||
val config = this.config ?: throw VpnException("Reconnect config is empty")
|
||||
start(config, vpnBuilder, protect, true)
|
||||
}
|
||||
}
|
||||
+12
-32
@@ -22,19 +22,15 @@ open class WireguardConfig protected constructor(
|
||||
val s2: Int?,
|
||||
val s3: Int?,
|
||||
val s4: Int?,
|
||||
val h1: Long?,
|
||||
val h2: Long?,
|
||||
val h3: Long?,
|
||||
val h4: Long?,
|
||||
val h1: String?,
|
||||
val h2: String?,
|
||||
val h3: String?,
|
||||
val h4: String?,
|
||||
var i1: String?,
|
||||
var i2: String?,
|
||||
var i3: String?,
|
||||
var i4: String?,
|
||||
var i5: String?,
|
||||
var j1: String?,
|
||||
var j2: String?,
|
||||
var j3: String?,
|
||||
var itime: Int?
|
||||
) : ProtocolConfig(protocolConfigBuilder) {
|
||||
|
||||
protected constructor(builder: Builder) : this(
|
||||
@@ -61,10 +57,6 @@ open class WireguardConfig protected constructor(
|
||||
builder.i3,
|
||||
builder.i4,
|
||||
builder.i5,
|
||||
builder.j1,
|
||||
builder.j2,
|
||||
builder.j3,
|
||||
builder.itime
|
||||
)
|
||||
|
||||
fun toWgUserspaceString(): String = with(StringBuilder()) {
|
||||
@@ -94,10 +86,6 @@ open class WireguardConfig protected constructor(
|
||||
i3?.let { appendLine("i3=$it") }
|
||||
i4?.let { appendLine("i4=$it") }
|
||||
i5?.let { appendLine("i5=$it") }
|
||||
j1?.let { appendLine("j1=$it") }
|
||||
j2?.let { appendLine("j2=$it") }
|
||||
j3?.let { appendLine("j3=$it") }
|
||||
itime?.let { appendLine("itime=$it") }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,19 +140,15 @@ open class WireguardConfig protected constructor(
|
||||
internal var s2: Int? = null
|
||||
internal var s3: Int? = null
|
||||
internal var s4: Int? = null
|
||||
internal var h1: Long? = null
|
||||
internal var h2: Long? = null
|
||||
internal var h3: Long? = null
|
||||
internal var h4: Long? = null
|
||||
internal var h1: String? = null
|
||||
internal var h2: String? = null
|
||||
internal var h3: String? = null
|
||||
internal var h4: String? = null
|
||||
internal var i1: String? = null
|
||||
internal var i2: String? = null
|
||||
internal var i3: String? = null
|
||||
internal var i4: String? = null
|
||||
internal var i5: String? = null
|
||||
internal var j1: String? = null
|
||||
internal var j2: String? = null
|
||||
internal var j3: String? = null
|
||||
internal var itime: Int? = null
|
||||
|
||||
fun setEndpoint(endpoint: InetEndpoint) = apply { this.endpoint = endpoint }
|
||||
|
||||
@@ -185,19 +169,15 @@ open class WireguardConfig protected constructor(
|
||||
fun setS2(s2: Int) = apply { this.s2 = s2 }
|
||||
fun setS3(s3: Int) = apply { this.s3 = s3 }
|
||||
fun setS4(s4: Int) = apply { this.s4 = s4 }
|
||||
fun setH1(h1: Long) = apply { this.h1 = h1 }
|
||||
fun setH2(h2: Long) = apply { this.h2 = h2 }
|
||||
fun setH3(h3: Long) = apply { this.h3 = h3 }
|
||||
fun setH4(h4: Long) = apply { this.h4 = h4 }
|
||||
fun setH1(h1: String) = apply { this.h1 = h1 }
|
||||
fun setH2(h2: String) = apply { this.h2 = h2 }
|
||||
fun setH3(h3: String) = apply { this.h3 = h3 }
|
||||
fun setH4(h4: String) = apply { this.h4 = h4 }
|
||||
fun setI1(i1: String) = apply { this.i1 = i1 }
|
||||
fun setI2(i2: String) = apply { this.i2 = i2 }
|
||||
fun setI3(i3: String) = apply { this.i3 = i3 }
|
||||
fun setI4(i4: String) = apply { this.i4 = i4 }
|
||||
fun setI5(i5: String) = apply { this.i5 = i5 }
|
||||
fun setJ1(j1: String) = apply { this.j1 = j1 }
|
||||
fun setJ2(j2: String) = apply { this.j2 = j2 }
|
||||
fun setJ3(j3: String) = apply { this.j3 = j3 }
|
||||
fun setItime(itime: Int) = apply { this.itime = itime }
|
||||
|
||||
override fun build(): WireguardConfig = configBuild().run { WireguardConfig(this@Builder) }
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ class Xray : Protocol() {
|
||||
state.value = DISCONNECTED
|
||||
}
|
||||
|
||||
override fun reconnectVpn(vpnBuilder: Builder) {
|
||||
override fun reconnectVpn(vpnBuilder: Builder, protect: (Int) -> Boolean) {
|
||||
state.value = CONNECTED
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ class Xray : Protocol() {
|
||||
mtu = config.mtu.toLong()
|
||||
proxy = "socks5://127.0.0.1:${config.socksPort}"
|
||||
device = "fd://$fd"
|
||||
logLevel = "warning"
|
||||
logLevel = "warn"
|
||||
}
|
||||
LibXray.startTun2Socks(tun2SocksConfig, fd.toLong()).isNotNullOrBlank { err ->
|
||||
throw VpnStartException("Failed to start tun2socks: $err")
|
||||
|
||||
@@ -38,7 +38,7 @@ elseif(APPLE AND NOT IOS)
|
||||
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")
|
||||
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")
|
||||
@@ -62,7 +62,7 @@ elseif(LINUX)
|
||||
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})
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
message("Client android ${CMAKE_ANDROID_ARCH_ABI} build")
|
||||
|
||||
set(APP_ANDROID_MIN_SDK 26)
|
||||
set(APP_ANDROID_MIN_SDK 28)
|
||||
set(ANDROID_PLATFORM "android-${APP_ANDROID_MIN_SDK}" CACHE STRING
|
||||
"The minimum API level supported by the application or library" FORCE)
|
||||
|
||||
@@ -11,8 +11,8 @@ set_target_properties(${PROJECT} PROPERTIES
|
||||
QT_ANDROID_VERSION_NAME ${CMAKE_PROJECT_VERSION}
|
||||
QT_ANDROID_VERSION_CODE ${APP_ANDROID_VERSION_CODE}
|
||||
QT_ANDROID_MIN_SDK_VERSION ${APP_ANDROID_MIN_SDK}
|
||||
QT_ANDROID_TARGET_SDK_VERSION 34
|
||||
QT_ANDROID_SDK_BUILD_TOOLS_REVISION 34.0.0
|
||||
QT_ANDROID_TARGET_SDK_VERSION 36
|
||||
QT_ANDROID_SDK_BUILD_TOOLS_REVISION 36.0.0
|
||||
QT_ANDROID_PACKAGE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/android
|
||||
)
|
||||
|
||||
@@ -20,7 +20,11 @@ set(QT_ANDROID_MULTI_ABI_FORWARD_VARS "QT_NO_GLOBAL_APK_TARGET_PART_OF_ALL;CMAKE
|
||||
|
||||
# We need to include qtprivate api's
|
||||
# As QAndroidBinder is not yet implemented with a public api
|
||||
set(LIBS ${LIBS} Qt6::CorePrivate -ljnigraphics)
|
||||
# Check if Qt6::CorePrivate is available (may not be in all Qt versions/configurations)
|
||||
if(TARGET Qt6::CorePrivate)
|
||||
set(LIBS ${LIBS} Qt6::CorePrivate)
|
||||
endif()
|
||||
set(LIBS ${LIBS} -ljnigraphics)
|
||||
|
||||
link_directories(${CMAKE_CURRENT_SOURCE_DIR}/platforms/android)
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ set(HEADERS ${HEADERS}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate-C-Interface.h
|
||||
)
|
||||
set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h PROPERTIES OBJECTIVE_CPP_HEADER TRUE)
|
||||
@@ -46,6 +47,8 @@ set(SOURCES ${SOURCES}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/AmneziaSceneDelegateHooks.mm
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ set(HEADERS ${HEADERS}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate-C-Interface.h
|
||||
)
|
||||
@@ -45,6 +46,7 @@ set(SOURCES ${SOURCES}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm
|
||||
|
||||
@@ -23,11 +23,18 @@ 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
|
||||
${CLIENT_ROOT_DIR}/utils/qmlUtils.h
|
||||
${CLIENT_ROOT_DIR}/core/api/apiUtils.h
|
||||
${CLIENT_ROOT_DIR}/core/osSignalHandler.h
|
||||
)
|
||||
|
||||
# Mozilla headres
|
||||
@@ -36,7 +43,6 @@ set(HEADERS ${HEADERS}
|
||||
${CLIENT_ROOT_DIR}/mozilla/shared/ipaddress.h
|
||||
${CLIENT_ROOT_DIR}/mozilla/shared/leakdetector.h
|
||||
${CLIENT_ROOT_DIR}/mozilla/controllerimpl.h
|
||||
${CLIENT_ROOT_DIR}/mozilla/localsocketcontroller.h
|
||||
)
|
||||
|
||||
if(NOT IOS AND NOT MACOS_NE)
|
||||
@@ -68,6 +74,11 @@ 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
|
||||
@@ -79,6 +90,7 @@ set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/../common/logger/logger.cpp
|
||||
${CLIENT_ROOT_DIR}/utils/qmlUtils.cpp
|
||||
${CLIENT_ROOT_DIR}/core/api/apiUtils.cpp
|
||||
${CLIENT_ROOT_DIR}/core/osSignalHandler.cpp
|
||||
)
|
||||
|
||||
# Mozilla sources
|
||||
@@ -86,7 +98,6 @@ set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/mozilla/models/server.cpp
|
||||
${CLIENT_ROOT_DIR}/mozilla/shared/ipaddress.cpp
|
||||
${CLIENT_ROOT_DIR}/mozilla/shared/leakdetector.cpp
|
||||
${CLIENT_ROOT_DIR}/mozilla/localsocketcontroller.cpp
|
||||
)
|
||||
|
||||
if(NOT IOS AND NOT MACOS_NE)
|
||||
@@ -175,7 +186,7 @@ if(WIN32)
|
||||
)
|
||||
endif()
|
||||
|
||||
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
|
||||
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
|
||||
message("Client desktop build")
|
||||
add_compile_definitions(AMNEZIA_DESKTOP)
|
||||
|
||||
@@ -189,11 +200,13 @@ if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
|
||||
${CLIENT_ROOT_DIR}/protocols/wireguardprotocol.h
|
||||
${CLIENT_ROOT_DIR}/protocols/xrayprotocol.h
|
||||
${CLIENT_ROOT_DIR}/protocols/awgprotocol.h
|
||||
${CLIENT_ROOT_DIR}/mozilla/localsocketcontroller.h
|
||||
)
|
||||
|
||||
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
|
||||
${CLIENT_ROOT_DIR}/protocols/openvpnovercloakprotocol.cpp
|
||||
@@ -203,3 +216,14 @@ if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
|
||||
${CLIENT_ROOT_DIR}/protocols/awgprotocol.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
if(APPLE AND MACOS_NE)
|
||||
# Include only the tray notification handler in NE builds
|
||||
set(HEADERS ${HEADERS}
|
||||
${CLIENT_ROOT_DIR}/ui/systemtray_notificationhandler.h
|
||||
)
|
||||
|
||||
set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/ui/systemtray_notificationhandler.cpp
|
||||
)
|
||||
endif()
|
||||
@@ -41,18 +41,16 @@ QString AwgConfigurator::createConfig(const ServerCredentials &credentials, Dock
|
||||
jsonConfig[config_key::underloadPacketMagicHeader] = configMap.value(config_key::underloadPacketMagicHeader);
|
||||
jsonConfig[config_key::transportPacketMagicHeader] = configMap.value(config_key::transportPacketMagicHeader);
|
||||
|
||||
// jsonConfig[config_key::cookieReplyPacketJunkSize] = configMap.value(config_key::cookieReplyPacketJunkSize);
|
||||
// jsonConfig[config_key::transportPacketJunkSize] = configMap.value(config_key::transportPacketJunkSize);
|
||||
if (container == DockerContainer::Awg2) {
|
||||
jsonConfig[config_key::cookieReplyPacketJunkSize] = configMap.value(config_key::cookieReplyPacketJunkSize);
|
||||
jsonConfig[config_key::transportPacketJunkSize] = configMap.value(config_key::transportPacketJunkSize);
|
||||
}
|
||||
|
||||
// jsonConfig[config_key::specialJunk1] = configMap.value(amnezia::config_key::specialJunk1);
|
||||
// jsonConfig[config_key::specialJunk2] = configMap.value(amnezia::config_key::specialJunk2);
|
||||
// jsonConfig[config_key::specialJunk3] = configMap.value(amnezia::config_key::specialJunk3);
|
||||
// jsonConfig[config_key::specialJunk4] = configMap.value(amnezia::config_key::specialJunk4);
|
||||
// jsonConfig[config_key::specialJunk5] = configMap.value(amnezia::config_key::specialJunk5);
|
||||
// jsonConfig[config_key::controlledJunk1] = configMap.value(amnezia::config_key::controlledJunk1);
|
||||
// jsonConfig[config_key::controlledJunk2] = configMap.value(amnezia::config_key::controlledJunk2);
|
||||
// jsonConfig[config_key::controlledJunk3] = configMap.value(amnezia::config_key::controlledJunk3);
|
||||
// jsonConfig[config_key::specialHandshakeTimeout] = configMap.value(amnezia::config_key::specialHandshakeTimeout);
|
||||
jsonConfig[config_key::specialJunk1] = configMap.value(amnezia::config_key::specialJunk1);
|
||||
jsonConfig[config_key::specialJunk2] = configMap.value(amnezia::config_key::specialJunk2);
|
||||
jsonConfig[config_key::specialJunk3] = configMap.value(amnezia::config_key::specialJunk3);
|
||||
jsonConfig[config_key::specialJunk4] = configMap.value(amnezia::config_key::specialJunk4);
|
||||
jsonConfig[config_key::specialJunk5] = configMap.value(amnezia::config_key::specialJunk5);
|
||||
|
||||
jsonConfig[config_key::mtu] =
|
||||
containerConfig.value(ProtocolProps::protoToString(Proto::Awg)).toObject().value(config_key::mtu).toString(protocols::awg::defaultMtu);
|
||||
|
||||
@@ -83,12 +83,30 @@ QString OpenVpnConfigurator::createConfig(const ServerCredentials &credentials,
|
||||
return "";
|
||||
}
|
||||
|
||||
auto sanitizeStaticKey = [](const QString &key) {
|
||||
QStringList lines = key.split('\n');
|
||||
QStringList filtered;
|
||||
filtered.reserve(lines.size());
|
||||
for (const QString &line : lines) {
|
||||
const QString trimmed = line.trimmed();
|
||||
if (trimmed.startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
filtered.append(line);
|
||||
}
|
||||
QString result = filtered.join('\n');
|
||||
if (!result.endsWith('\n')) {
|
||||
result.append('\n');
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
config.replace("$OPENVPN_CA_CERT", connData.caCert);
|
||||
config.replace("$OPENVPN_CLIENT_CERT", connData.clientCert);
|
||||
config.replace("$OPENVPN_PRIV_KEY", connData.privKey);
|
||||
|
||||
if (config.contains("$OPENVPN_TA_KEY")) {
|
||||
config.replace("$OPENVPN_TA_KEY", connData.taKey);
|
||||
config.replace("$OPENVPN_TA_KEY", sanitizeStaticKey(connData.taKey));
|
||||
} else {
|
||||
config.replace("<tls-auth>", "");
|
||||
config.replace("</tls-auth>", "");
|
||||
@@ -117,7 +135,7 @@ QString OpenVpnConfigurator::processConfigWithLocalSettings(const QPair<QString,
|
||||
if (!isApiConfig) {
|
||||
QRegularExpression regex("redirect-gateway.*");
|
||||
config.replace(regex, "");
|
||||
|
||||
|
||||
// We don't use secondary DNS if primary DNS is AmneziaDNS
|
||||
if (dns.first.contains(protocols::dns::amneziaDnsIp)) {
|
||||
QRegularExpression dnsRegex("dhcp-option DNS " + dns.second);
|
||||
|
||||
@@ -103,7 +103,11 @@ WireguardConfigurator::ConnectionData WireguardConfigurator::prepareWireguardCon
|
||||
return connData;
|
||||
}
|
||||
|
||||
QString getIpsScript = QString("cat %1 | grep AllowedIPs").arg(m_serverConfigPath);
|
||||
QString configPath = m_serverConfigPath;
|
||||
if (container == DockerContainer::Awg) {
|
||||
configPath = amnezia::protocols::awg::serverLegacyConfigPath;
|
||||
}
|
||||
QString getIpsScript = QString("cat %1 | grep AllowedIPs").arg(configPath);
|
||||
QString stdOut;
|
||||
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
|
||||
stdOut += data + "\n";
|
||||
@@ -161,15 +165,18 @@ WireguardConfigurator::ConnectionData WireguardConfigurator::prepareWireguardCon
|
||||
"AllowedIPs = %3/32\n\n")
|
||||
.arg(connData.clientPubKey, connData.pskKey, connData.clientIP);
|
||||
|
||||
errorCode = m_serverController->uploadTextFileToContainer(container, credentials, configPart, m_serverConfigPath,
|
||||
errorCode = m_serverController->uploadTextFileToContainer(container, credentials, configPart, configPath,
|
||||
libssh::ScpOverwriteMode::ScpAppendToExisting);
|
||||
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return connData;
|
||||
}
|
||||
|
||||
QString script = QString("sudo docker exec -i $CONTAINER_NAME bash -c 'wg syncconf wg0 <(wg-quick strip %1)'")
|
||||
.arg(m_serverConfigPath);
|
||||
bool isAwg = (container == DockerContainer::Awg2);
|
||||
QString bin = isAwg ? QStringLiteral("awg") : QStringLiteral("wg");
|
||||
QString iface = isAwg ? QStringLiteral("awg0") : QStringLiteral("wg0");
|
||||
QString script = QString(
|
||||
"sudo docker exec -i $CONTAINER_NAME bash -c '%1 syncconf %2 <(%1-quick strip %3)'").arg(bin, iface, configPath);
|
||||
|
||||
errorCode = m_serverController->runScript(
|
||||
credentials,
|
||||
|
||||
@@ -28,7 +28,10 @@ QString ContainerProps::containerToString(amnezia::DockerContainer c)
|
||||
return "none";
|
||||
if (c == DockerContainer::Cloak)
|
||||
return "amnezia-openvpn-cloak";
|
||||
|
||||
if (c == DockerContainer::Awg)
|
||||
return "amnezia-awg";
|
||||
if (c == DockerContainer::Awg2)
|
||||
return "amnezia-awg2";
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<DockerContainer>();
|
||||
QString containerKey = metaEnum.valueToKey(static_cast<int>(c));
|
||||
|
||||
@@ -41,7 +44,10 @@ QString ContainerProps::containerTypeToString(amnezia::DockerContainer c)
|
||||
return "none";
|
||||
if (c == DockerContainer::Ipsec)
|
||||
return "ikev2";
|
||||
|
||||
if (c == DockerContainer::Awg)
|
||||
return "awg";
|
||||
if (c == DockerContainer::Awg2)
|
||||
return "awg";
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<DockerContainer>();
|
||||
QString containerKey = metaEnum.valueToKey(static_cast<int>(c));
|
||||
|
||||
@@ -71,6 +77,8 @@ QVector<amnezia::Proto> ContainerProps::protocolsForContainer(amnezia::DockerCon
|
||||
|
||||
case DockerContainer::Socks5Proxy: return { Proto::Socks5Proxy };
|
||||
|
||||
case DockerContainer::Awg: return { Proto::Awg };
|
||||
case DockerContainer::Awg2: return { Proto::Awg };
|
||||
default: return { defaultProtocol(container) };
|
||||
}
|
||||
}
|
||||
@@ -94,6 +102,7 @@ QMap<DockerContainer, QString> ContainerProps::containerHumanNames()
|
||||
{ DockerContainer::Cloak, "OpenVPN over Cloak" },
|
||||
{ DockerContainer::WireGuard, "WireGuard" },
|
||||
{ DockerContainer::Awg, "AmneziaWG" },
|
||||
{ DockerContainer::Awg2, "AmneziaWG" },
|
||||
{ DockerContainer::Xray, "XRay" },
|
||||
{ DockerContainer::Ipsec, QObject::tr("IPsec") },
|
||||
{ DockerContainer::SSXray, "Shadowsocks"},
|
||||
@@ -120,6 +129,9 @@ QMap<DockerContainer, QString> ContainerProps::containerDescriptions()
|
||||
{ DockerContainer::Awg,
|
||||
QObject::tr("AmneziaWG is a special protocol from Amnezia based on WireGuard. "
|
||||
"It provides high connection speed and ensures stable operation even in the most challenging network conditions.") },
|
||||
{ DockerContainer::Awg2,
|
||||
QObject::tr("AmneziaWG is a special protocol from Amnezia based on WireGuard. "
|
||||
"It provides high connection speed and ensures stable operation even in the most challenging network conditions.") },
|
||||
{ DockerContainer::Xray,
|
||||
QObject::tr("XRay with REALITY masks VPN traffic as web traffic and protects against active probing. "
|
||||
"It is highly resistant to detection and offers high speed.") },
|
||||
@@ -182,7 +194,7 @@ QMap<DockerContainer, QString> ContainerProps::containerDetailedDescriptions()
|
||||
"* Minimal configuration required\n"
|
||||
"* Easily detected by DPI systems (susceptible to blocking)\n"
|
||||
"* Operates over UDP protocol") },
|
||||
{ DockerContainer::Awg,
|
||||
{ DockerContainer::Awg2,
|
||||
QObject::tr("AmneziaWG is a modern VPN protocol based on WireGuard, "
|
||||
"combining simplified architecture with high performance across all devices. "
|
||||
"It addresses WireGuard's main vulnerability (easy detection by DPI systems) through advanced obfuscation techniques, "
|
||||
@@ -242,6 +254,7 @@ Proto ContainerProps::defaultProtocol(DockerContainer c)
|
||||
case DockerContainer::Cloak: return Proto::Cloak;
|
||||
case DockerContainer::ShadowSocks: return Proto::ShadowSocks;
|
||||
case DockerContainer::WireGuard: return Proto::WireGuard;
|
||||
case DockerContainer::Awg2: return Proto::Awg;
|
||||
case DockerContainer::Awg: return Proto::Awg;
|
||||
case DockerContainer::Xray: return Proto::Xray;
|
||||
case DockerContainer::Ipsec: return Proto::Ikev2;
|
||||
@@ -255,6 +268,15 @@ Proto ContainerProps::defaultProtocol(DockerContainer c)
|
||||
}
|
||||
}
|
||||
|
||||
QString ContainerProps::containerTypeToProtocolString(DockerContainer c)
|
||||
{
|
||||
if (c == DockerContainer::None)
|
||||
return "none";
|
||||
|
||||
Proto p = defaultProtocol(c);
|
||||
return ProtocolProps::protoToString(p);
|
||||
}
|
||||
|
||||
bool ContainerProps::isSupportedByCurrentPlatform(DockerContainer c)
|
||||
{
|
||||
#ifdef Q_OS_WINDOWS
|
||||
@@ -265,6 +287,7 @@ bool ContainerProps::isSupportedByCurrentPlatform(DockerContainer c)
|
||||
switch (c) {
|
||||
case DockerContainer::WireGuard: return true;
|
||||
case DockerContainer::OpenVpn: return true;
|
||||
case DockerContainer::Awg2: return true;
|
||||
case DockerContainer::Awg: return true;
|
||||
case DockerContainer::Xray: return true;
|
||||
case DockerContainer::Cloak: return true;
|
||||
@@ -278,6 +301,7 @@ bool ContainerProps::isSupportedByCurrentPlatform(DockerContainer c)
|
||||
// macOS build using Network Extension – hide OpenVPN-based containers
|
||||
switch (c) {
|
||||
case DockerContainer::WireGuard: return true;
|
||||
case DockerContainer::Awg2: return true;
|
||||
case DockerContainer::Awg: return true;
|
||||
case DockerContainer::Xray: return true;
|
||||
case DockerContainer::SSXray: return true;
|
||||
@@ -300,6 +324,7 @@ bool ContainerProps::isSupportedByCurrentPlatform(DockerContainer c)
|
||||
case DockerContainer::WireGuard: return true;
|
||||
case DockerContainer::OpenVpn: return true;
|
||||
case DockerContainer::ShadowSocks: return false;
|
||||
case DockerContainer::Awg2: return true;
|
||||
case DockerContainer::Awg: return true;
|
||||
case DockerContainer::Cloak: return true;
|
||||
case DockerContainer::Xray: return true;
|
||||
@@ -329,7 +354,7 @@ QStringList ContainerProps::fixedPortsForContainer(DockerContainer c)
|
||||
bool ContainerProps::isEasySetupContainer(DockerContainer container)
|
||||
{
|
||||
switch (container) {
|
||||
case DockerContainer::Awg: return true;
|
||||
case DockerContainer::Awg2: return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
@@ -337,7 +362,7 @@ bool ContainerProps::isEasySetupContainer(DockerContainer container)
|
||||
QString ContainerProps::easySetupHeader(DockerContainer container)
|
||||
{
|
||||
switch (container) {
|
||||
case DockerContainer::Awg: return tr("Automatic");
|
||||
case DockerContainer::Awg2: return tr("Automatic");
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
@@ -345,7 +370,7 @@ QString ContainerProps::easySetupHeader(DockerContainer container)
|
||||
QString ContainerProps::easySetupDescription(DockerContainer container)
|
||||
{
|
||||
switch (container) {
|
||||
case DockerContainer::Awg: return tr("AmneziaWG protocol will be installed. "
|
||||
case DockerContainer::Awg2: return tr("AmneziaWG protocol will be installed. "
|
||||
"It provides high connection speed and ensures stable operation even in the most challenging network conditions.");
|
||||
default: return "";
|
||||
}
|
||||
@@ -354,7 +379,7 @@ QString ContainerProps::easySetupDescription(DockerContainer container)
|
||||
int ContainerProps::easySetupOrder(DockerContainer container)
|
||||
{
|
||||
switch (container) {
|
||||
case DockerContainer::Awg: return 1;
|
||||
case DockerContainer::Awg2: return 1;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
@@ -370,6 +395,12 @@ bool ContainerProps::isShareable(DockerContainer container)
|
||||
}
|
||||
}
|
||||
|
||||
bool ContainerProps::isAwgContainer(DockerContainer container)
|
||||
{
|
||||
return container == DockerContainer::Awg || container == DockerContainer::Awg2;
|
||||
}
|
||||
|
||||
|
||||
QJsonObject ContainerProps::getProtocolConfigFromContainer(const Proto protocol, const QJsonObject &containerConfig)
|
||||
{
|
||||
QString protocolConfigString = containerConfig.value(ProtocolProps::protoToString(protocol))
|
||||
@@ -387,7 +418,7 @@ int ContainerProps::installPageOrder(DockerContainer container)
|
||||
case DockerContainer::Cloak: return 5;
|
||||
case DockerContainer::ShadowSocks: return 6;
|
||||
case DockerContainer::WireGuard: return 2;
|
||||
case DockerContainer::Awg: return 1;
|
||||
case DockerContainer::Awg2: return 1;
|
||||
case DockerContainer::Xray: return 3;
|
||||
case DockerContainer::Ipsec: return 7;
|
||||
case DockerContainer::SSXray: return 8;
|
||||
|
||||
@@ -17,6 +17,7 @@ namespace amnezia
|
||||
enum DockerContainer {
|
||||
None = 0,
|
||||
Awg,
|
||||
Awg2,
|
||||
WireGuard,
|
||||
OpenVpn,
|
||||
Cloak,
|
||||
@@ -45,6 +46,7 @@ namespace amnezia
|
||||
Q_INVOKABLE static amnezia::DockerContainer containerFromString(const QString &container);
|
||||
Q_INVOKABLE static QString containerToString(amnezia::DockerContainer container);
|
||||
Q_INVOKABLE static QString containerTypeToString(amnezia::DockerContainer c);
|
||||
Q_INVOKABLE static QString containerTypeToProtocolString(amnezia::DockerContainer c);
|
||||
|
||||
Q_INVOKABLE static QList<amnezia::DockerContainer> allContainers();
|
||||
|
||||
@@ -71,6 +73,9 @@ namespace amnezia
|
||||
|
||||
static bool isShareable(amnezia::DockerContainer container);
|
||||
|
||||
static bool isAwgContainer(amnezia::DockerContainer container);
|
||||
|
||||
|
||||
static QJsonObject getProtocolConfigFromContainer(const amnezia::Proto protocol, const QJsonObject &containerConfig);
|
||||
|
||||
static int installPageOrder(amnezia::DockerContainer container);
|
||||
|
||||
@@ -47,12 +47,14 @@ namespace apiDefs
|
||||
constexpr QLatin1String serverCountryName("server_country_name");
|
||||
|
||||
constexpr QLatin1String osVersion("os_version");
|
||||
constexpr QLatin1String appLanguage("app_language");
|
||||
|
||||
constexpr QLatin1String availableCountries("available_countries");
|
||||
constexpr QLatin1String activeDeviceCount("active_device_count");
|
||||
constexpr QLatin1String maxDeviceCount("max_device_count");
|
||||
constexpr QLatin1String subscriptionEndDate("subscription_end_date");
|
||||
constexpr QLatin1String issuedConfigs("issued_configs");
|
||||
constexpr QLatin1String subscriptionDescription("subscription_description");
|
||||
|
||||
constexpr QLatin1String supportInfo("support_info");
|
||||
constexpr QLatin1String email("email");
|
||||
@@ -64,9 +66,20 @@ namespace apiDefs
|
||||
constexpr QLatin1String id("id");
|
||||
constexpr QLatin1String orderId("order_id");
|
||||
constexpr QLatin1String migrationCode("migration_code");
|
||||
|
||||
constexpr QLatin1String transactionId("transaction_id");
|
||||
constexpr QLatin1String isTestPurchase("is_test_purchase");
|
||||
|
||||
constexpr QLatin1String userCountryCode("user_country_code");
|
||||
|
||||
constexpr QLatin1String serviceInfo("service_info");
|
||||
constexpr QLatin1String isAdVisible("is_ad_visible");
|
||||
constexpr QLatin1String adHeader("ad_header");
|
||||
constexpr QLatin1String adDescription("ad_description");
|
||||
constexpr QLatin1String adEndpoint("ad_endpoint");
|
||||
}
|
||||
|
||||
const int requestTimeoutMsecs = 12 * 1000; // 12 secs
|
||||
const int requestTimeoutMsecs = 30 * 1000; // 30 secs (increased for DNS transport testing)
|
||||
}
|
||||
|
||||
#endif // APIDEFS_H
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "apiUtils.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
|
||||
namespace
|
||||
@@ -23,7 +24,7 @@ namespace
|
||||
|
||||
bool apiUtils::isSubscriptionExpired(const QString &subscriptionEndDate)
|
||||
{
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
QDateTime now = QDateTime::currentDateTimeUtc();
|
||||
QDateTime endDate = QDateTime::fromString(subscriptionEndDate, Qt::ISODateWithMs);
|
||||
return endDate < now;
|
||||
}
|
||||
@@ -82,34 +83,45 @@ apiDefs::ConfigSource apiUtils::getConfigSource(const QJsonObject &serverConfigO
|
||||
return static_cast<apiDefs::ConfigSource>(serverConfigObject.value(apiDefs::key::configVersion).toInt());
|
||||
}
|
||||
|
||||
amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &sslErrors, QNetworkReply *reply)
|
||||
amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &sslErrors, const QString &replyErrorString,
|
||||
const QNetworkReply::NetworkError &replyError, const int httpStatusCode,
|
||||
const QByteArray &responseBody)
|
||||
{
|
||||
const int httpStatusCodeConflict = 409;
|
||||
const int httpStatusCodeNotFound = 404;
|
||||
const int httpStatusCodeNotImplemented = 501;
|
||||
|
||||
if (!sslErrors.empty()) {
|
||||
qDebug().noquote() << sslErrors;
|
||||
return amnezia::ErrorCode::ApiConfigSslError;
|
||||
} else if (reply->error() == QNetworkReply::NoError) {
|
||||
} else if (replyError == QNetworkReply::NoError) {
|
||||
return amnezia::ErrorCode::NoError;
|
||||
} else if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError
|
||||
|| reply->error() == QNetworkReply::NetworkError::TimeoutError) {
|
||||
qDebug() << reply->error();
|
||||
} else if (replyError == QNetworkReply::NetworkError::OperationCanceledError
|
||||
|| replyError == QNetworkReply::NetworkError::TimeoutError) {
|
||||
qDebug() << replyError;
|
||||
return amnezia::ErrorCode::ApiConfigTimeoutError;
|
||||
} else if (reply->error() == QNetworkReply::NetworkError::OperationNotImplementedError) {
|
||||
qDebug() << reply->error();
|
||||
} else if (replyError == QNetworkReply::NetworkError::OperationNotImplementedError) {
|
||||
qDebug() << replyError;
|
||||
return amnezia::ErrorCode::ApiUpdateRequestError;
|
||||
} else {
|
||||
QString err = reply->errorString();
|
||||
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
qDebug() << QString::fromUtf8(reply->readAll());
|
||||
qDebug() << reply->error();
|
||||
qDebug() << err;
|
||||
qDebug() << QString::fromUtf8(responseBody);
|
||||
qDebug() << replyError;
|
||||
qDebug() << replyErrorString;
|
||||
qDebug() << httpStatusCode;
|
||||
if (httpStatusCode == httpStatusCodeConflict) {
|
||||
|
||||
int httpStatusFromBody = -1;
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
||||
if (jsonDoc.isObject()) {
|
||||
QJsonObject jsonObj = jsonDoc.object();
|
||||
httpStatusFromBody = jsonObj.value("http_status").toInt(-1);
|
||||
}
|
||||
|
||||
if (httpStatusFromBody == httpStatusCodeConflict) {
|
||||
return amnezia::ErrorCode::ApiConfigLimitError;
|
||||
} else if (httpStatusCode == httpStatusCodeNotFound) {
|
||||
} else if (httpStatusFromBody == httpStatusCodeNotFound) {
|
||||
return amnezia::ErrorCode::ApiNotFoundError;
|
||||
} else if (httpStatusFromBody == httpStatusCodeNotImplemented) {
|
||||
return amnezia::ErrorCode::ApiUpdateRequestError;
|
||||
}
|
||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
@@ -162,3 +174,51 @@ QString apiUtils::getPremiumV1VpnKey(const QJsonObject &serverConfigObject)
|
||||
|
||||
return QString("vpn://%1").arg(QString(signedData.toBase64(QByteArray::Base64UrlEncoding)));
|
||||
}
|
||||
|
||||
QString apiUtils::getPremiumV2VpnKey(const QJsonObject &serverConfigObject)
|
||||
{
|
||||
if (apiUtils::getConfigType(serverConfigObject) != apiDefs::ConfigType::AmneziaPremiumV2) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QString vpnKeyText = "";
|
||||
|
||||
auto apiConfig = serverConfigObject.value(apiDefs::key::apiConfig).toObject();
|
||||
auto authData = serverConfigObject.value(QLatin1String("auth_data")).toObject();
|
||||
|
||||
const QString name = serverConfigObject.value(apiDefs::key::name).toString();
|
||||
const QString description = serverConfigObject.value(apiDefs::key::description).toString();
|
||||
const double configVersion = serverConfigObject.value(apiDefs::key::configVersion).toDouble();
|
||||
|
||||
const QString serviceType = apiConfig.value(apiDefs::key::serviceType).toString();
|
||||
const QString serviceProtocol = apiConfig.value(QLatin1String("service_protocol")).toString();
|
||||
const QString userCountryCode = apiConfig.value(QLatin1String("user_country_code")).toString();
|
||||
|
||||
const QString apiKey = authData.value(apiDefs::key::apiKey).toString();
|
||||
|
||||
QString vpnKeyStr = "{";
|
||||
vpnKeyStr += "\"" + QString(apiDefs::key::name) + "\": \"" + name + "\", ";
|
||||
vpnKeyStr += "\"" + QString(apiDefs::key::description) + "\": \"" + description + "\", ";
|
||||
vpnKeyStr += "\"" + QString(apiDefs::key::configVersion) + "\": " + QString::number(static_cast<int>(configVersion)) + ", ";
|
||||
|
||||
vpnKeyStr += "\"" + QString(apiDefs::key::apiConfig) + "\": {";
|
||||
vpnKeyStr += "\"" + QString(apiDefs::key::serviceType) + "\": \"" + serviceType + "\", ";
|
||||
vpnKeyStr += "\"service_protocol\": \"" + serviceProtocol + "\", ";
|
||||
vpnKeyStr += "\"user_country_code\": \"" + userCountryCode + "\"";
|
||||
vpnKeyStr += "}, ";
|
||||
|
||||
vpnKeyStr += "\"auth_data\": {";
|
||||
vpnKeyStr += "\"" + QString(apiDefs::key::apiKey) + "\": \"" + apiKey + "\"";
|
||||
vpnKeyStr += "}";
|
||||
|
||||
vpnKeyStr += "}";
|
||||
|
||||
QByteArray vpnKeyCompressed = escapeUnicode(vpnKeyStr).toUtf8();
|
||||
vpnKeyCompressed = qCompress(vpnKeyCompressed, 6);
|
||||
vpnKeyCompressed = vpnKeyCompressed.mid(4);
|
||||
|
||||
QByteArray signedData = AMNEZIA_CONFIG_SIGNATURE + vpnKeyCompressed;
|
||||
vpnKeyText = QString("vpn://%1").arg(QString(signedData.toBase64(QByteArray::Base64UrlEncoding)));
|
||||
|
||||
return vpnKeyText;
|
||||
}
|
||||
@@ -18,9 +18,12 @@ namespace apiUtils
|
||||
apiDefs::ConfigType getConfigType(const QJsonObject &serverConfigObject);
|
||||
apiDefs::ConfigSource getConfigSource(const QJsonObject &serverConfigObject);
|
||||
|
||||
amnezia::ErrorCode checkNetworkReplyErrors(const QList<QSslError> &sslErrors, QNetworkReply *reply);
|
||||
amnezia::ErrorCode checkNetworkReplyErrors(const QList<QSslError> &sslErrors, const QString &replyErrorString,
|
||||
const QNetworkReply::NetworkError &replyError, const int httpStatusCode,
|
||||
const QByteArray &responseBody);
|
||||
|
||||
QString getPremiumV1VpnKey(const QJsonObject &serverConfigObject);
|
||||
QString getPremiumV2VpnKey(const QJsonObject &serverConfigObject);
|
||||
}
|
||||
|
||||
#endif // APIUTILS_H
|
||||
@@ -26,9 +26,8 @@ CoreController::CoreController(const QSharedPointer<VpnConnection> &vpnConnectio
|
||||
|
||||
initNotificationHandler();
|
||||
|
||||
auto locale = m_settings->getAppLanguage();
|
||||
m_translator.reset(new QTranslator());
|
||||
updateTranslator(locale);
|
||||
updateTranslator(m_settings->getAppLanguage());
|
||||
}
|
||||
|
||||
void CoreController::initModels()
|
||||
@@ -100,6 +99,9 @@ void CoreController::initModels()
|
||||
|
||||
m_apiDevicesModel.reset(new ApiDevicesModel(m_settings, this));
|
||||
m_engine->rootContext()->setContextProperty("ApiDevicesModel", m_apiDevicesModel.get());
|
||||
|
||||
m_newsModel.reset(new NewsModel(m_settings, this));
|
||||
m_engine->rootContext()->setContextProperty("NewsModel", m_newsModel.get());
|
||||
}
|
||||
|
||||
void CoreController::initControllers()
|
||||
@@ -152,8 +154,8 @@ void CoreController::initControllers()
|
||||
m_apiConfigsController.reset(new ApiConfigsController(m_serversModel, m_apiServicesModel, m_settings));
|
||||
m_engine->rootContext()->setContextProperty("ApiConfigsController", m_apiConfigsController.get());
|
||||
|
||||
m_apiPremV1MigrationController.reset(new ApiPremV1MigrationController(m_serversModel, m_settings, this));
|
||||
m_engine->rootContext()->setContextProperty("ApiPremV1MigrationController", m_apiPremV1MigrationController.get());
|
||||
m_apiNewsController.reset(new ApiNewsController(m_newsModel, m_settings, m_serversModel, this));
|
||||
m_engine->rootContext()->setContextProperty("ApiNewsController", m_apiNewsController.get());
|
||||
}
|
||||
|
||||
void CoreController::initAndroidController()
|
||||
@@ -226,14 +228,12 @@ void CoreController::initSignalHandlers()
|
||||
initAutoConnectHandler();
|
||||
initAmneziaDnsToggledHandler();
|
||||
initPrepareConfigHandler();
|
||||
initImportPremiumV2VpnKeyHandler();
|
||||
initShowMigrationDrawerHandler();
|
||||
initStrictKillSwitchHandler();
|
||||
}
|
||||
|
||||
void CoreController::initNotificationHandler()
|
||||
{
|
||||
#ifndef Q_OS_ANDROID
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
m_notificationHandler.reset(NotificationHandler::create(nullptr));
|
||||
|
||||
connect(m_vpnConnection.get(), &VpnConnection::connectionStateChanged, m_notificationHandler.get(),
|
||||
@@ -317,6 +317,11 @@ void CoreController::initContainerModelUpdateHandler()
|
||||
connect(m_serversModel.get(), &ServersModel::containersUpdated, m_containersModel.get(), &ContainersModel::updateModel);
|
||||
connect(m_serversModel.get(), &ServersModel::defaultServerContainersUpdated, m_defaultServerContainersModel.get(),
|
||||
&ContainersModel::updateModel);
|
||||
connect(m_serversModel.get(), &ServersModel::gatewayStacksExpanded, this, [this]() {
|
||||
if (m_serversModel->hasServersFromGatewayApi()) {
|
||||
m_apiNewsController->fetchNews(false);
|
||||
}
|
||||
});
|
||||
m_serversModel->resetModel();
|
||||
}
|
||||
|
||||
@@ -372,25 +377,6 @@ void CoreController::initPrepareConfigHandler()
|
||||
});
|
||||
}
|
||||
|
||||
void CoreController::initImportPremiumV2VpnKeyHandler()
|
||||
{
|
||||
connect(m_apiPremV1MigrationController.get(), &ApiPremV1MigrationController::importPremiumV2VpnKey, this, [this](const QString &vpnKey) {
|
||||
m_importController->extractConfigFromData(vpnKey);
|
||||
m_importController->importConfig();
|
||||
|
||||
emit m_apiPremV1MigrationController->migrationFinished();
|
||||
});
|
||||
}
|
||||
|
||||
void CoreController::initShowMigrationDrawerHandler()
|
||||
{
|
||||
QTimer::singleShot(1000, this, [this]() {
|
||||
if (m_apiPremV1MigrationController->isPremV1MigrationReminderActive() && m_apiPremV1MigrationController->hasConfigsToMigration()) {
|
||||
m_apiPremV1MigrationController->showMigrationDrawer();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void CoreController::initStrictKillSwitchHandler()
|
||||
{
|
||||
connect(m_settingsController.get(), &SettingsController::strictKillSwitchEnabledChanged, m_vpnConnection.get(),
|
||||
@@ -401,3 +387,22 @@ QSharedPointer<PageController> CoreController::pageController() const
|
||||
{
|
||||
return m_pageController;
|
||||
}
|
||||
|
||||
void CoreController::openConnectionByIndex(int serverIndex)
|
||||
{
|
||||
if (m_serversModel) {
|
||||
m_serversModel->setProcessedServerIndex(serverIndex);
|
||||
m_serversModel->setDefaultServerIndex(serverIndex);
|
||||
}
|
||||
m_connectionController->toggleConnection();
|
||||
}
|
||||
|
||||
void CoreController::importConfigFromData(const QString &data)
|
||||
{
|
||||
if (!m_importController)
|
||||
return;
|
||||
|
||||
if (m_importController->extractConfigFromData(data)) {
|
||||
m_importController->importConfig();
|
||||
}
|
||||
}
|
||||
@@ -5,13 +5,13 @@
|
||||
#include <QQmlContext>
|
||||
#include <QThread>
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
#include "ui/systemtray_notificationhandler.h"
|
||||
#endif
|
||||
|
||||
#include "ui/controllers/api/apiConfigsController.h"
|
||||
#include "ui/controllers/api/apiSettingsController.h"
|
||||
#include "ui/controllers/api/apiPremV1MigrationController.h"
|
||||
#include "ui/controllers/api/apiNewsController.h"
|
||||
#include "ui/controllers/appSplitTunnelingController.h"
|
||||
#include "ui/controllers/allowedDnsController.h"
|
||||
#include "ui/controllers/connectionController.h"
|
||||
@@ -47,8 +47,9 @@
|
||||
#include "ui/models/services/sftpConfigModel.h"
|
||||
#include "ui/models/services/socks5ProxyConfigModel.h"
|
||||
#include "ui/models/sites_model.h"
|
||||
#include "ui/models/newsModel.h"
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
#include "ui/notificationhandler.h"
|
||||
#endif
|
||||
|
||||
@@ -63,6 +64,9 @@ public:
|
||||
QSharedPointer<PageController> pageController() const;
|
||||
void setQmlRoot();
|
||||
|
||||
void openConnectionByIndex(int serverIndex);
|
||||
void importConfigFromData(const QString &data);
|
||||
|
||||
signals:
|
||||
void translationsUpdated();
|
||||
void websiteUrlChanged(const QString &newUrl);
|
||||
@@ -88,8 +92,6 @@ private:
|
||||
void initAutoConnectHandler();
|
||||
void initAmneziaDnsToggledHandler();
|
||||
void initPrepareConfigHandler();
|
||||
void initImportPremiumV2VpnKeyHandler();
|
||||
void initShowMigrationDrawerHandler();
|
||||
void initStrictKillSwitchHandler();
|
||||
|
||||
QQmlApplicationEngine *m_engine {}; // TODO use parent child system here?
|
||||
@@ -97,7 +99,7 @@ private:
|
||||
QSharedPointer<VpnConnection> m_vpnConnection;
|
||||
QSharedPointer<QTranslator> m_translator;
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
QScopedPointer<NotificationHandler> m_notificationHandler;
|
||||
#endif
|
||||
|
||||
@@ -117,7 +119,7 @@ private:
|
||||
|
||||
QScopedPointer<ApiSettingsController> m_apiSettingsController;
|
||||
QScopedPointer<ApiConfigsController> m_apiConfigsController;
|
||||
QScopedPointer<ApiPremV1MigrationController> m_apiPremV1MigrationController;
|
||||
QScopedPointer<ApiNewsController> m_apiNewsController;
|
||||
|
||||
QSharedPointer<ContainersModel> m_containersModel;
|
||||
QSharedPointer<ContainersModel> m_defaultServerContainersModel;
|
||||
@@ -125,6 +127,7 @@ private:
|
||||
QSharedPointer<LanguageModel> m_languageModel;
|
||||
QSharedPointer<ProtocolsModel> m_protocolsModel;
|
||||
QSharedPointer<SitesModel> m_sitesModel;
|
||||
QSharedPointer<NewsModel> m_newsModel;
|
||||
QSharedPointer<AllowedDnsModel> m_allowedDnsModel;
|
||||
QSharedPointer<AppSplitTunnelingModel> m_appSplitTunnelingModel;
|
||||
QSharedPointer<ClientManagementModel> m_clientManagementModel;
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
#include "gatewayController.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <random>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
#include <QUrl>
|
||||
#include <QMutexLocker>
|
||||
#include <QSharedPointer>
|
||||
#include <QThread>
|
||||
#include <QtConcurrent>
|
||||
|
||||
#include "QBlockCipher.h"
|
||||
#include "QRsa.h"
|
||||
|
||||
#include "amnezia_application.h"
|
||||
#include "core/api/apiUtils.h"
|
||||
#include "core/networkUtilities.h"
|
||||
#include "core/transport/dnsGatewayTransport.h"
|
||||
#include "core/transport/httpGatewayTransport.h"
|
||||
#include "utilities.h"
|
||||
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
#include "core/ipcclient.h"
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
namespace configKey
|
||||
@@ -33,332 +29,330 @@ namespace
|
||||
constexpr char keyPayload[] = "key_payload";
|
||||
}
|
||||
|
||||
constexpr QLatin1String errorResponsePattern1("No active configuration found for");
|
||||
constexpr QLatin1String errorResponsePattern2("No non-revoked public key found for");
|
||||
constexpr QLatin1String errorResponsePattern3("Account not found.");
|
||||
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 updateRequestResponsePattern("client version update is required");
|
||||
TransportsConfig TransportsConfig::fromJson(const QJsonObject &json)
|
||||
{
|
||||
using amnezia::transport::dns::DnsProtocol;
|
||||
|
||||
TransportsConfig config;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
ErrorCode GatewayController::get(const QString &endpoint, QByteArray &responseBody)
|
||||
std::shared_ptr<amnezia::transport::IGatewayTransport> GatewayController::buildTransport(
|
||||
const TransportsConfig &config, int requestTimeoutMsecs, bool isDevEnvironment, bool isStrictKillSwitchEnabled)
|
||||
{
|
||||
#ifdef Q_OS_IOS
|
||||
IosController::Instance()->requestInetAccess();
|
||||
QThread::msleep(10);
|
||||
#endif
|
||||
using namespace amnezia::transport;
|
||||
|
||||
QNetworkRequest request;
|
||||
request.setTransferTimeout(m_requestTimeoutMsecs);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
auto makeHttp = [&](const QString &httpEndpoint) {
|
||||
return std::make_shared<HttpGatewayTransport>(
|
||||
httpEndpoint, isDevEnvironment, requestTimeoutMsecs, isStrictKillSwitchEnabled);
|
||||
};
|
||||
|
||||
request.setUrl(QString(endpoint).arg(m_gatewayEndpoint));
|
||||
if (config.primary == PrimaryTransport::Http) {
|
||||
return makeHttp(config.httpEndpoint);
|
||||
}
|
||||
|
||||
// bypass killSwitch exceptions for API-gateway
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
if (m_isStrictKillSwitchEnabled) {
|
||||
QString host = QUrl(request.url()).host();
|
||||
QString ip = NetworkUtilities::getIPAddress(host);
|
||||
if (!ip.isEmpty()) {
|
||||
IpcClient::Interface()->addKillSwitchAllowedRange(QStringList { ip });
|
||||
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);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
QNetworkReply *reply;
|
||||
reply = amnApp->networkManager()->get(request);
|
||||
|
||||
QEventLoop wait;
|
||||
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
||||
|
||||
QList<QSslError> sslErrors;
|
||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||
wait.exec();
|
||||
|
||||
responseBody = reply->readAll();
|
||||
|
||||
if (sslErrors.isEmpty() && shouldBypassProxy(reply, responseBody, false)) {
|
||||
auto requestFunction = [&request, &responseBody](const QString &url) {
|
||||
request.setUrl(url);
|
||||
return amnApp->networkManager()->get(request);
|
||||
};
|
||||
|
||||
auto replyProcessingFunction = [&responseBody, &reply, &sslErrors, this](QNetworkReply *nestedReply,
|
||||
const QList<QSslError> &nestedSslErrors) {
|
||||
responseBody = nestedReply->readAll();
|
||||
if (!sslErrors.isEmpty() || !shouldBypassProxy(nestedReply, responseBody, false)) {
|
||||
sslErrors = nestedSslErrors;
|
||||
reply = nestedReply;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
bypassProxy(endpoint, reply, requestFunction, replyProcessingFunction);
|
||||
}
|
||||
|
||||
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, reply);
|
||||
reply->deleteLater();
|
||||
|
||||
return errorCode;
|
||||
return makeHttp(config.httpEndpoint);
|
||||
}
|
||||
|
||||
ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
|
||||
void GatewayController::setTransportsConfig(const TransportsConfig &config)
|
||||
{
|
||||
#ifdef Q_OS_IOS
|
||||
IosController::Instance()->requestInetAccess();
|
||||
QThread::msleep(10);
|
||||
#endif
|
||||
|
||||
QNetworkRequest request;
|
||||
request.setTransferTimeout(m_requestTimeoutMsecs);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
|
||||
request.setUrl(endpoint.arg(m_gatewayEndpoint));
|
||||
|
||||
// bypass killSwitch exceptions for API-gateway
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
if (m_isStrictKillSwitchEnabled) {
|
||||
QString host = QUrl(request.url()).host();
|
||||
QString ip = NetworkUtilities::getIPAddress(host);
|
||||
if (!ip.isEmpty()) {
|
||||
IpcClient::Interface()->addKillSwitchAllowedRange(QStringList { ip });
|
||||
}
|
||||
if (config.timeoutMs > 0) {
|
||||
m_requestTimeoutMsecs = config.timeoutMs;
|
||||
}
|
||||
#endif
|
||||
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;
|
||||
|
||||
QSimpleCrypto::QBlockCipher blockCipher;
|
||||
QByteArray key = blockCipher.generatePrivateSalt(32);
|
||||
QByteArray iv = blockCipher.generatePrivateSalt(32);
|
||||
QByteArray salt = blockCipher.generatePrivateSalt(8);
|
||||
result.key = blockCipher.generatePrivateSalt(32);
|
||||
result.iv = blockCipher.generatePrivateSalt(16);
|
||||
result.salt = blockCipher.generatePrivateSalt(8);
|
||||
|
||||
QJsonObject keyPayload;
|
||||
keyPayload[configKey::aesKey] = QString(key.toBase64());
|
||||
keyPayload[configKey::aesIv] = QString(iv.toBase64());
|
||||
keyPayload[configKey::aesSalt] = QString(salt.toBase64());
|
||||
keyPayload[configKey::aesKey] = QString(result.key.toBase64());
|
||||
keyPayload[configKey::aesIv] = QString(result.iv.toBase64());
|
||||
keyPayload[configKey::aesSalt] = QString(result.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;
|
||||
QSimpleCrypto::QRsa rsa;
|
||||
rsaKey = rsaKey.trimmed();
|
||||
rsaKey.replace("\\n", "\n");
|
||||
publicKey = rsa.getPublicKeyFromByteArray(rsaKey);
|
||||
} catch (...) {
|
||||
Utils::logException();
|
||||
qCritical() << "error loading public key from environment variables";
|
||||
return ErrorCode::ApiMissingAgwPublicKey;
|
||||
result.errorCode = amnezia::ErrorCode::ApiMissingAgwPublicKey;
|
||||
return result;
|
||||
}
|
||||
|
||||
encryptedKeyPayload = rsa.encrypt(QJsonDocument(keyPayload).toJson(), publicKey, RSA_PKCS1_PADDING);
|
||||
encryptedKeyPayload = rsa.encrypt(QJsonDocument(keyPayload).toJson(QJsonDocument::Compact),
|
||||
publicKey, RSA_PKCS1_PADDING);
|
||||
EVP_PKEY_free(publicKey);
|
||||
|
||||
encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(), key, iv, "", salt);
|
||||
} catch (...) { // todo change error handling in QSimpleCrypto?
|
||||
encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(QJsonDocument::Compact),
|
||||
result.key, result.iv, "", result.salt);
|
||||
} catch (...) {
|
||||
Utils::logException();
|
||||
qCritical() << "error when encrypting the request body";
|
||||
return ErrorCode::ApiConfigDecryptionError;
|
||||
result.errorCode = amnezia::ErrorCode::ApiConfigDecryptionError;
|
||||
return result;
|
||||
}
|
||||
|
||||
QJsonObject requestBody;
|
||||
requestBody[configKey::keyPayload] = QString(encryptedKeyPayload.toBase64());
|
||||
requestBody[configKey::apiPayload] = QString(encryptedApiPayload.toBase64());
|
||||
|
||||
QNetworkReply *reply = amnApp->networkManager()->post(request, QJsonDocument(requestBody).toJson());
|
||||
result.body = QJsonDocument(requestBody).toJson(QJsonDocument::Compact);
|
||||
return result;
|
||||
}
|
||||
|
||||
QEventLoop wait;
|
||||
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
||||
amnezia::transport::DecryptionResult GatewayController::decryptResponse(const QByteArray &encryptedResponseBody,
|
||||
const QByteArray &key,
|
||||
const QByteArray &iv,
|
||||
const QByteArray &salt) const
|
||||
{
|
||||
amnezia::transport::DecryptionResult result;
|
||||
result.decrypted = encryptedResponseBody;
|
||||
result.isOk = false;
|
||||
|
||||
QList<QSslError> sslErrors;
|
||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||
wait.exec();
|
||||
|
||||
QByteArray encryptedResponseBody = reply->readAll();
|
||||
|
||||
if (sslErrors.isEmpty() && shouldBypassProxy(reply, encryptedResponseBody, true, key, iv, salt)) {
|
||||
auto requestFunction = [&request, &encryptedResponseBody, &requestBody](const QString &url) {
|
||||
request.setUrl(url);
|
||||
return amnApp->networkManager()->post(request, QJsonDocument(requestBody).toJson());
|
||||
};
|
||||
|
||||
auto replyProcessingFunction = [&encryptedResponseBody, &reply, &sslErrors, &key, &iv, &salt,
|
||||
this](QNetworkReply *nestedReply, const QList<QSslError> &nestedSslErrors) {
|
||||
encryptedResponseBody = nestedReply->readAll();
|
||||
reply = nestedReply;
|
||||
if (!sslErrors.isEmpty() || shouldBypassProxy(nestedReply, encryptedResponseBody, true, key, iv, salt)) {
|
||||
sslErrors = nestedSslErrors;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
bypassProxy(endpoint, reply, requestFunction, replyProcessingFunction);
|
||||
}
|
||||
|
||||
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, reply);
|
||||
reply->deleteLater();
|
||||
if (errorCode) {
|
||||
return errorCode;
|
||||
if (encryptedResponseBody.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
responseBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt);
|
||||
return ErrorCode::NoError;
|
||||
} catch (...) { // todo change error handling in QSimpleCrypto?
|
||||
Utils::logException();
|
||||
qCritical() << "error when decrypting the request body";
|
||||
return ErrorCode::ApiConfigDecryptionError;
|
||||
QSimpleCrypto::QBlockCipher blockCipher;
|
||||
result.decrypted = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt);
|
||||
result.isOk = true;
|
||||
} catch (...) {
|
||||
result.decrypted = encryptedResponseBody;
|
||||
result.isOk = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
QStringList GatewayController::getProxyUrls()
|
||||
std::shared_ptr<amnezia::transport::IGatewayTransport> GatewayController::currentTransport() const
|
||||
{
|
||||
QNetworkRequest request;
|
||||
request.setTransferTimeout(m_requestTimeoutMsecs);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
|
||||
QEventLoop wait;
|
||||
QList<QSslError> sslErrors;
|
||||
QNetworkReply *reply;
|
||||
|
||||
QStringList proxyStorageUrls;
|
||||
if (m_isDevEnvironment) {
|
||||
proxyStorageUrls = QString(DEV_S3_ENDPOINT).split(", ");
|
||||
} else {
|
||||
proxyStorageUrls = QString(PROD_S3_ENDPOINT).split(", ");
|
||||
}
|
||||
|
||||
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
|
||||
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();
|
||||
|
||||
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 {
|
||||
apiUtils::checkNetworkReplyErrors(sslErrors, reply);
|
||||
qDebug() << "go to the next storage endpoint";
|
||||
|
||||
reply->deleteLater();
|
||||
}
|
||||
}
|
||||
return {};
|
||||
QMutexLocker lock(&m_transportMutex);
|
||||
return m_transport;
|
||||
}
|
||||
|
||||
bool GatewayController::shouldBypassProxy(QNetworkReply *reply, const QByteArray &responseBody, bool checkEncryption, const QByteArray &key,
|
||||
const QByteArray &iv, const QByteArray &salt)
|
||||
amnezia::ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
|
||||
{
|
||||
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError || reply->error() == QNetworkReply::NetworkError::TimeoutError) {
|
||||
qDebug() << "timeout occurred";
|
||||
qDebug() << reply->error();
|
||||
return true;
|
||||
} else if (responseBody.contains("html")) {
|
||||
qDebug() << "the response contains an html tag";
|
||||
return true;
|
||||
} else if (reply->error() == QNetworkReply::NetworkError::ContentNotFoundError) {
|
||||
if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2)
|
||||
|| responseBody.contains(errorResponsePattern3)) {
|
||||
return false;
|
||||
} else {
|
||||
qDebug() << reply->error();
|
||||
return true;
|
||||
}
|
||||
} else if (reply->error() == QNetworkReply::NetworkError::OperationNotImplementedError) {
|
||||
if (responseBody.contains(updateRequestResponsePattern)) {
|
||||
return false;
|
||||
} else {
|
||||
qDebug() << reply->error();
|
||||
return true;
|
||||
}
|
||||
} else if (reply->error() != QNetworkReply::NetworkError::NoError) {
|
||||
qDebug() << reply->error();
|
||||
return true;
|
||||
} else if (checkEncryption) {
|
||||
try {
|
||||
QSimpleCrypto::QBlockCipher blockCipher;
|
||||
static_cast<void>(blockCipher.decryptAesBlockCipher(responseBody, key, iv, "", salt));
|
||||
} catch (...) {
|
||||
qDebug() << "failed to decrypt the data";
|
||||
return true;
|
||||
}
|
||||
EncryptedRequest enc = encryptRequest(apiPayload);
|
||||
if (enc.errorCode != amnezia::ErrorCode::NoError) {
|
||||
return enc.errorCode;
|
||||
}
|
||||
return false;
|
||||
|
||||
auto transport = currentTransport();
|
||||
if (!transport) {
|
||||
return amnezia::ErrorCode::AmneziaServiceConnectionFailed;
|
||||
}
|
||||
|
||||
auto decryptionHook = [this, key = enc.key, iv = enc.iv, salt = enc.salt](const QByteArray &encrypted) {
|
||||
return decryptResponse(encrypted, key, iv, salt);
|
||||
};
|
||||
|
||||
return transport->send(endpoint, enc.body, responseBody, decryptionHook);
|
||||
}
|
||||
|
||||
void GatewayController::bypassProxy(const QString &endpoint, QNetworkReply *reply,
|
||||
std::function<QNetworkReply *(const QString &url)> requestFunction,
|
||||
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction)
|
||||
QFuture<QPair<amnezia::ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload)
|
||||
{
|
||||
QStringList proxyUrls = getProxyUrls();
|
||||
std::random_device randomDevice;
|
||||
std::mt19937 generator(randomDevice());
|
||||
std::shuffle(proxyUrls.begin(), proxyUrls.end(), generator);
|
||||
|
||||
QEventLoop wait;
|
||||
QList<QSslError> sslErrors;
|
||||
QByteArray responseBody;
|
||||
|
||||
for (const QString &proxyUrl : proxyUrls) {
|
||||
qDebug() << "go to the next proxy endpoint";
|
||||
reply->deleteLater(); // delete the previous reply
|
||||
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();
|
||||
|
||||
if (replyProcessingFunction(reply, sslErrors)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return QtConcurrent::run([this, endpoint, apiPayload]() {
|
||||
QByteArray responseBody;
|
||||
amnezia::ErrorCode errorCode = post(endpoint, apiPayload, responseBody);
|
||||
return qMakePair(errorCode, responseBody);
|
||||
});
|
||||
}
|
||||
@@ -1,37 +1,88 @@
|
||||
#ifndef GATEWAYCONTROLLER_H
|
||||
#define GATEWAYCONTROLLER_H
|
||||
|
||||
#include <QNetworkReply>
|
||||
#include <QFuture>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QMutex>
|
||||
#include <QObject>
|
||||
#include <QPair>
|
||||
#include <memory>
|
||||
|
||||
#include "core/defs.h"
|
||||
#include "core/transport/dns/dnsResolver.h"
|
||||
#include "core/transport/igatewaytransport.h"
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#endif
|
||||
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);
|
||||
};
|
||||
|
||||
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 get(const QString &endpoint, QByteArray &responseBody);
|
||||
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:
|
||||
QStringList getProxyUrls();
|
||||
bool shouldBypassProxy(QNetworkReply *reply, const QByteArray &responseBody, bool checkEncryption, const QByteArray &key = "",
|
||||
const QByteArray &iv = "", const QByteArray &salt = "");
|
||||
void bypassProxy(const QString &endpoint, QNetworkReply *reply, std::function<QNetworkReply *(const QString &url)> requestFunction,
|
||||
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction);
|
||||
struct EncryptedRequest
|
||||
{
|
||||
QByteArray body;
|
||||
QByteArray key;
|
||||
QByteArray iv;
|
||||
QByteArray salt;
|
||||
amnezia::ErrorCode errorCode = amnezia::ErrorCode::NoError;
|
||||
};
|
||||
|
||||
EncryptedRequest encryptRequest(const QJsonObject &apiPayload);
|
||||
amnezia::transport::DecryptionResult decryptResponse(const QByteArray &encryptedResponseBody,
|
||||
const QByteArray &key,
|
||||
const QByteArray &iv,
|
||||
const QByteArray &salt) const;
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
#endif // GATEWAYCONTROLLER_H
|
||||
@@ -345,7 +345,7 @@ bool ServerController::isReinstallContainerRequired(DockerContainer container, c
|
||||
return true;
|
||||
}
|
||||
|
||||
if (container == DockerContainer::Awg) {
|
||||
if (ContainerProps::isAwgContainer(container)) {
|
||||
if ((oldProtoConfig.value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress)
|
||||
!= newProtoConfig.value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress))
|
||||
|| (oldProtoConfig.value(config_key::port).toString(protocols::awg::defaultPort)
|
||||
@@ -367,11 +367,11 @@ bool ServerController::isReinstallContainerRequired(DockerContainer container, c
|
||||
|| (oldProtoConfig.value(config_key::underloadPacketMagicHeader).toString(protocols::awg::defaultUnderloadPacketMagicHeader)
|
||||
!= newProtoConfig.value(config_key::underloadPacketMagicHeader).toString(protocols::awg::defaultUnderloadPacketMagicHeader))
|
||||
|| (oldProtoConfig.value(config_key::transportPacketMagicHeader).toString(protocols::awg::defaultTransportPacketMagicHeader))
|
||||
!= newProtoConfig.value(config_key::transportPacketMagicHeader).toString(protocols::awg::defaultTransportPacketMagicHeader))
|
||||
// || (oldProtoConfig.value(config_key::cookieReplyPacketJunkSize).toString(protocols::awg::defaultCookieReplyPacketJunkSize)
|
||||
// != newProtoConfig.value(config_key::cookieReplyPacketJunkSize).toString(protocols::awg::defaultCookieReplyPacketJunkSize))
|
||||
// || (oldProtoConfig.value(config_key::transportPacketJunkSize).toString(protocols::awg::defaultTransportPacketJunkSize)
|
||||
// != newProtoConfig.value(config_key::transportPacketJunkSize).toString(protocols::awg::defaultTransportPacketJunkSize))
|
||||
!= newProtoConfig.value(config_key::transportPacketMagicHeader).toString(protocols::awg::defaultTransportPacketMagicHeader)
|
||||
|| (oldProtoConfig.value(config_key::cookieReplyPacketJunkSize).toString(protocols::awg::defaultCookieReplyPacketJunkSize)
|
||||
!= newProtoConfig.value(config_key::cookieReplyPacketJunkSize).toString(protocols::awg::defaultCookieReplyPacketJunkSize))
|
||||
|| (oldProtoConfig.value(config_key::transportPacketJunkSize).toString(protocols::awg::defaultTransportPacketJunkSize)
|
||||
!= newProtoConfig.value(config_key::transportPacketJunkSize).toString(protocols::awg::defaultTransportPacketJunkSize)))
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -648,6 +648,11 @@ ServerController::Vars ServerController::genVarsForScript(const ServerCredential
|
||||
|
||||
vars.append({ { "$COOKIE_REPLY_PACKET_JUNK_SIZE", amneziaWireguarConfig.value(config_key::cookieReplyPacketJunkSize).toString() } });
|
||||
vars.append({ { "$TRANSPORT_PACKET_JUNK_SIZE", amneziaWireguarConfig.value(config_key::transportPacketJunkSize).toString() } });
|
||||
vars.append({ { "$SPECIAL_JUNK_1", amneziaWireguarConfig.value(config_key::specialJunk1).toString() } });
|
||||
vars.append({ { "$SPECIAL_JUNK_2", amneziaWireguarConfig.value(config_key::specialJunk2).toString() } });
|
||||
vars.append({ { "$SPECIAL_JUNK_3", amneziaWireguarConfig.value(config_key::specialJunk3).toString() } });
|
||||
vars.append({ { "$SPECIAL_JUNK_4", amneziaWireguarConfig.value(config_key::specialJunk4).toString() } });
|
||||
vars.append({ { "$SPECIAL_JUNK_5", amneziaWireguarConfig.value(config_key::specialJunk5).toString() } });
|
||||
|
||||
// Socks5 proxy vars
|
||||
vars.append({ { "$SOCKS5_PROXY_PORT", socks5ProxyConfig.value(config_key::port).toString(protocols::socks5Proxy::defaultPort) } });
|
||||
@@ -657,7 +662,8 @@ ServerController::Vars ServerController::genVarsForScript(const ServerCredential
|
||||
vars.append({ { "$SOCKS5_USER", socks5user } });
|
||||
vars.append({ { "$SOCKS5_AUTH_TYPE", socks5user.isEmpty() ? "none" : "strong" } });
|
||||
|
||||
QString serverIp = (container != DockerContainer::Awg && container != DockerContainer::WireGuard && container != DockerContainer::Xray)
|
||||
QString serverIp = (!ContainerProps::isAwgContainer(container) &&
|
||||
container != DockerContainer::WireGuard && container != DockerContainer::Xray)
|
||||
? NetworkUtilities::getIPAddress(credentials.hostName)
|
||||
: credentials.hostName;
|
||||
if (!serverIp.isEmpty()) {
|
||||
|
||||
@@ -99,11 +99,12 @@ QJsonObject VpnConfigurationsController::createVpnConfiguration(const QPair<QStr
|
||||
protocolConfigString = configurator->processConfigWithLocalSettings(dns, isApiConfig, protocolConfigString);
|
||||
|
||||
QJsonObject vpnConfigData = QJsonDocument::fromJson(protocolConfigString.toUtf8()).object();
|
||||
if (container == DockerContainer::Awg || container == DockerContainer::WireGuard) {
|
||||
if (ContainerProps::isAwgContainer(container) || container == DockerContainer::WireGuard) {
|
||||
// add mtu for old configs
|
||||
if (vpnConfigData[config_key::mtu].toString().isEmpty()) {
|
||||
vpnConfigData[config_key::mtu] =
|
||||
container == DockerContainer::Awg ? protocols::awg::defaultMtu : protocols::wireguard::defaultMtu;
|
||||
ContainerProps::isAwgContainer(container) ? protocols::awg::defaultMtu :
|
||||
protocols::wireguard::defaultMtu;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -120,6 +120,8 @@ namespace amnezia
|
||||
ApiNotFoundError = 1109,
|
||||
ApiMigrationError = 1110,
|
||||
ApiUpdateRequestError = 1111,
|
||||
ApiSubscriptionExpiredError = 1112,
|
||||
ApiPurchaseError = 1113,
|
||||
|
||||
// QFile errors
|
||||
OpenError = 1200,
|
||||
|
||||
@@ -77,6 +77,8 @@ QString errorString(ErrorCode code) {
|
||||
case (ErrorCode::ApiNotFoundError): errorMessage = QObject::tr("Error when retrieving configuration from API"); break;
|
||||
case (ErrorCode::ApiMigrationError): errorMessage = QObject::tr("A migration error has occurred. Please contact our technical support"); break;
|
||||
case (ErrorCode::ApiUpdateRequestError): errorMessage = QObject::tr("Please update the application to use this feature"); break;
|
||||
case (ErrorCode::ApiSubscriptionExpiredError): errorMessage = QObject::tr("Your Amnezia Premium subscription has expired.\n Please check your email for renewal instructions.\n If you haven't received an email, please contact our support."); break;
|
||||
case (ErrorCode::ApiPurchaseError): errorMessage = QObject::tr("Unable to process purchase"); break;
|
||||
|
||||
// QFile errors
|
||||
case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
|
||||
|
||||
+47
-80
@@ -1,109 +1,71 @@
|
||||
#include "ipcclient.h"
|
||||
#include "ipc.h"
|
||||
#include <QRemoteObjectNode>
|
||||
|
||||
IpcClient *IpcClient::m_instance = nullptr;
|
||||
#include <QtNetwork/qlocalsocket.h>
|
||||
|
||||
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()
|
||||
IpcClient& IpcClient::Instance()
|
||||
{
|
||||
if (m_localSocket)
|
||||
m_localSocket->close();
|
||||
}
|
||||
|
||||
bool IpcClient::isSocketConnected() const
|
||||
{
|
||||
return m_isSocketConnected;
|
||||
}
|
||||
|
||||
IpcClient *IpcClient::Instance()
|
||||
{
|
||||
return m_instance;
|
||||
thread_local IpcClient ipcClient;
|
||||
return ipcClient;
|
||||
}
|
||||
|
||||
QSharedPointer<IpcInterfaceReplica> IpcClient::Interface()
|
||||
{
|
||||
if (!Instance())
|
||||
QSharedPointer<IpcInterfaceReplica> rep = Instance().m_interface;
|
||||
if (rep.isNull()) {
|
||||
qCritical() << "IpcClient::Interface(): Failed to acquire replica";
|
||||
return nullptr;
|
||||
return Instance()->m_ipcClient;
|
||||
}
|
||||
if (!rep->waitForSource(1000)) {
|
||||
qCritical() << "IpcClient::Interface(): Failed to initialize replica";
|
||||
return nullptr;
|
||||
}
|
||||
if (!rep->isReplicaValid()) {
|
||||
qWarning() << "IpcClient::Interface(): Replica is invalid";
|
||||
}
|
||||
return rep;
|
||||
}
|
||||
|
||||
QSharedPointer<IpcProcessTun2SocksReplica> IpcClient::InterfaceTun2Socks()
|
||||
{
|
||||
if (!Instance())
|
||||
QSharedPointer<IpcProcessTun2SocksReplica> rep = Instance().m_tun2socks;
|
||||
if (rep.isNull()) {
|
||||
qCritical() << "IpcClient::InterfaceTun2Socks: Replica is undefined";
|
||||
return nullptr;
|
||||
return Instance()->m_Tun2SocksClient;
|
||||
}
|
||||
|
||||
bool IpcClient::init(IpcClient *instance)
|
||||
{
|
||||
m_instance = instance;
|
||||
|
||||
Instance()->m_localSocket = new QLocalSocket(Instance());
|
||||
connect(Instance()->m_localSocket.data(), &QLocalSocket::connected, &Instance()->m_ClientNode, []() {
|
||||
Instance()->m_ClientNode.addClientSideConnection(Instance()->m_localSocket.data());
|
||||
auto cliNode = Instance()->m_ClientNode.acquire<IpcInterfaceReplica>();
|
||||
cliNode->waitForSource(5000);
|
||||
Instance()->m_ipcClient.reset(cliNode);
|
||||
|
||||
if (!Instance()->m_ipcClient) {
|
||||
qWarning() << "IpcClient is not ready!";
|
||||
}
|
||||
|
||||
Instance()->m_ipcClient->waitForSource(1000);
|
||||
|
||||
if (!Instance()->m_ipcClient->isReplicaValid()) {
|
||||
qWarning() << "IpcClient replica is not connected!";
|
||||
}
|
||||
|
||||
auto t2sNode = Instance()->m_ClientNode.acquire<IpcProcessTun2SocksReplica>();
|
||||
t2sNode->waitForSource(5000);
|
||||
Instance()->m_Tun2SocksClient.reset(t2sNode);
|
||||
|
||||
if (!Instance()->m_Tun2SocksClient) {
|
||||
qWarning() << "IpcClient::m_Tun2SocksClient is not ready!";
|
||||
}
|
||||
|
||||
Instance()->m_Tun2SocksClient->waitForSource(1000);
|
||||
|
||||
if (!Instance()->m_Tun2SocksClient->isReplicaValid()) {
|
||||
qWarning() << "IpcClient::m_Tun2SocksClient replica is not connected!";
|
||||
}
|
||||
});
|
||||
|
||||
connect(Instance()->m_localSocket, &QLocalSocket::disconnected,
|
||||
[instance]() { instance->m_isSocketConnected = false; });
|
||||
|
||||
Instance()->m_localSocket->connectToServer(amnezia::getIpcServiceUrl());
|
||||
Instance()->m_localSocket->waitForConnected();
|
||||
|
||||
if (!Instance()->m_ipcClient) {
|
||||
qDebug() << "IpcClient::init failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
qDebug() << "IpcClient::init succeed";
|
||||
instance->m_isSocketConnected = (Instance()->m_ipcClient->isReplicaValid() && Instance()->m_Tun2SocksClient->isReplicaValid());
|
||||
|
||||
return Instance()->isSocketConnected();
|
||||
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()
|
||||
{
|
||||
if (!Instance()->m_ipcClient || !Instance()->m_ipcClient->isReplicaValid()) {
|
||||
qWarning() << "IpcClient::createPrivilegedProcess : IpcClient IpcClient replica is not valid";
|
||||
QSharedPointer<IpcInterfaceReplica> rep = Interface();
|
||||
if (!rep) {
|
||||
qCritical() << "IpcClient::createPrivilegedProcess: Replica is invalid";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QRemoteObjectPendingReply<int> futureResult = Instance()->m_ipcClient->createPrivilegedProcess();
|
||||
futureResult.waitForFinished(5000);
|
||||
QRemoteObjectPendingReply<int> pidReply = rep->createPrivilegedProcess();
|
||||
if (!pidReply.waitForFinished(5000)){
|
||||
qCritical() << "IpcClient::createPrivilegedProcess: Failed to execute RO createPrivilegedProcess call";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int pid = futureResult.returnValue();
|
||||
|
||||
auto pd = QSharedPointer<ProcessDescriptor>(new ProcessDescriptor());
|
||||
Instance()->m_processNodes.insert(pid, pd);
|
||||
int pid = pidReply.returnValue();
|
||||
QSharedPointer<ProcessDescriptor> pd(new ProcessDescriptor());
|
||||
|
||||
pd->localSocket.reset(new QLocalSocket(pd->replicaNode.data()));
|
||||
|
||||
@@ -111,6 +73,7 @@ QSharedPointer<PrivilegedProcess> IpcClient::CreatePrivilegedProcess()
|
||||
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) {
|
||||
@@ -125,8 +88,12 @@ QSharedPointer<PrivilegedProcess> IpcClient::CreatePrivilegedProcess()
|
||||
[pd]() { pd->replicaNode->deleteLater(); });
|
||||
}
|
||||
});
|
||||
|
||||
pd->localSocket->connectToServer(amnezia::getIpcProcessUrl(pid));
|
||||
pd->localSocket->waitForConnected();
|
||||
if (!pd->localSocket->waitForConnected()) {
|
||||
qCritical() << "IpcClient::createPrivilegedProcess: Failed to connect to process' socket";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto processReplica = QSharedPointer<PrivilegedProcess>(pd->ipcProcess);
|
||||
return processReplica;
|
||||
|
||||
+36
-21
@@ -4,7 +4,6 @@
|
||||
#include <QLocalSocket>
|
||||
#include <QObject>
|
||||
|
||||
#include "ipc.h"
|
||||
#include "rep_ipc_interface_replica.h"
|
||||
#include "rep_ipc_process_tun2socks_replica.h"
|
||||
|
||||
@@ -14,27 +13,48 @@ class IpcClient : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit IpcClient(QObject *parent = nullptr);
|
||||
explicit IpcClient(QObject *parent = nullptr);
|
||||
|
||||
static IpcClient *Instance();
|
||||
static bool init(IpcClient *instance);
|
||||
static QSharedPointer<IpcInterfaceReplica> Interface();
|
||||
static QSharedPointer<IpcProcessTun2SocksReplica> InterfaceTun2Socks();
|
||||
static QSharedPointer<PrivilegedProcess> CreatePrivilegedProcess();
|
||||
static IpcClient& Instance();
|
||||
|
||||
bool isSocketConnected() const;
|
||||
static QSharedPointer<IpcInterfaceReplica> Interface();
|
||||
static QSharedPointer<IpcProcessTun2SocksReplica> InterfaceTun2Socks();
|
||||
static QSharedPointer<PrivilegedProcess> CreatePrivilegedProcess();
|
||||
|
||||
template <typename Func>
|
||||
static auto withInterface(Func func)
|
||||
{
|
||||
QSharedPointer<IpcInterfaceReplica> iface = Instance().m_interface;
|
||||
using ReturnType = decltype(func(std::declval<QSharedPointer<IpcInterfaceReplica>>()));
|
||||
|
||||
if (iface.isNull() || !iface->waitForSource(1000) || !iface->isReplicaValid()) {
|
||||
qWarning() << "IpcClient::withInterface(): Service is not running";
|
||||
|
||||
if constexpr (std::is_void_v<ReturnType>)
|
||||
return;
|
||||
else
|
||||
return ReturnType{};
|
||||
}
|
||||
|
||||
return func(iface);
|
||||
}
|
||||
|
||||
template <typename OnSuccess, typename OnFailure>
|
||||
static auto withInterface(OnSuccess onSuccess, OnFailure onFailure)
|
||||
{
|
||||
QSharedPointer<IpcInterfaceReplica> iface = Instance().m_interface;
|
||||
if (iface.isNull() || !iface->waitForSource(1000) || !iface->isReplicaValid()) {
|
||||
return onFailure();
|
||||
}
|
||||
|
||||
return onSuccess(iface);
|
||||
}
|
||||
signals:
|
||||
|
||||
private:
|
||||
~IpcClient() override;
|
||||
|
||||
QRemoteObjectNode m_ClientNode;
|
||||
QRemoteObjectNode m_Tun2SocksNode;
|
||||
QSharedPointer<IpcInterfaceReplica> m_ipcClient;
|
||||
QPointer<QLocalSocket> m_localSocket;
|
||||
QPointer<QLocalSocket> m_tun2socksSocket;
|
||||
QSharedPointer<IpcProcessTun2SocksReplica> m_Tun2SocksClient;
|
||||
QRemoteObjectNode m_node;
|
||||
QSharedPointer<IpcInterfaceReplica> m_interface;
|
||||
QSharedPointer<IpcProcessTun2SocksReplica> m_tun2socks;
|
||||
|
||||
struct ProcessDescriptor {
|
||||
ProcessDescriptor () {
|
||||
@@ -46,11 +66,6 @@ private:
|
||||
QSharedPointer<QRemoteObjectNode> replicaNode;
|
||||
QSharedPointer<QLocalSocket> localSocket;
|
||||
};
|
||||
|
||||
QMap<int, QSharedPointer<ProcessDescriptor>> m_processNodes;
|
||||
bool m_isSocketConnected {false};
|
||||
|
||||
static IpcClient *m_instance;
|
||||
};
|
||||
|
||||
#endif // IPCCLIENT_H
|
||||
@@ -1,11 +1,12 @@
|
||||
#include "networkUtilities.h"
|
||||
#include <QtNetwork/qnetworkinterface.h>
|
||||
#include <cstddef>
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#include <windows.h>
|
||||
#include <Ipexport.h>
|
||||
#include <Ws2tcpip.h>
|
||||
#include <ws2ipdef.h>
|
||||
#include <stdint.h>
|
||||
#include <Iphlpapi.h>
|
||||
#include <Iptypes.h>
|
||||
#include <WinSock2.h>
|
||||
@@ -30,10 +31,20 @@
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <net/route.h>
|
||||
#include <ifaddrs.h>
|
||||
#include <net/if.h>
|
||||
#include <net/if_dl.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <unistd.h>
|
||||
#include <ifaddrs.h>
|
||||
#include <net/if.h>
|
||||
#endif
|
||||
|
||||
#include <QHostAddress>
|
||||
#include <QHostInfo>
|
||||
#include <QDebug>
|
||||
|
||||
QRegularExpression NetworkUtilities::ipAddressRegExp()
|
||||
{
|
||||
@@ -170,7 +181,7 @@ int NetworkUtilities::AdapterIndexTo(const QHostAddress& dst) {
|
||||
#ifdef Q_OS_WIN
|
||||
qDebug() << "Getting Current Internet Adapter that routes to"
|
||||
<< dst.toString();
|
||||
quint32_be ipBigEndian;
|
||||
quint32 ipBigEndian;
|
||||
quint32 ip = dst.toIPv4Address();
|
||||
qToBigEndian(ip, &ipBigEndian);
|
||||
_MIB_IPFORWARDROW routeInfo;
|
||||
@@ -239,12 +250,14 @@ DWORD GetAdaptersAddressesWrapper(const ULONG Family,
|
||||
}
|
||||
#endif
|
||||
|
||||
QString NetworkUtilities::getGatewayAndIface()
|
||||
QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
|
||||
{
|
||||
#ifdef Q_OS_WIN
|
||||
constexpr int BUFF_LEN = 100;
|
||||
char buff[BUFF_LEN] = {'\0'};
|
||||
QString result;
|
||||
|
||||
QString resGateway;
|
||||
int resIndex = -1;
|
||||
|
||||
PIP_ADAPTER_ADDRESSES pAdapterAddresses = nullptr;
|
||||
DWORD dwRetVal =
|
||||
@@ -252,7 +265,7 @@ QString NetworkUtilities::getGatewayAndIface()
|
||||
|
||||
if (dwRetVal != NO_ERROR) {
|
||||
qDebug() << "ipv4 stack detect GetAdaptersAddresses failed.";
|
||||
return "";
|
||||
return {};
|
||||
}
|
||||
|
||||
PIP_ADAPTER_ADDRESSES pCurAddress = pAdapterAddresses;
|
||||
@@ -267,7 +280,9 @@ QString NetworkUtilities::getGatewayAndIface()
|
||||
struct sockaddr_in addr;
|
||||
if (inet_pton(AF_INET, buff, &addr.sin_addr) == 1) {
|
||||
qDebug() << "this is true v4 !";
|
||||
result = gw;
|
||||
|
||||
resGateway = gw;
|
||||
resIndex = pCurAddress->IfIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -275,7 +290,7 @@ QString NetworkUtilities::getGatewayAndIface()
|
||||
}
|
||||
|
||||
free(pAdapterAddresses);
|
||||
return result;
|
||||
return { resGateway, QNetworkInterface::interfaceFromIndex(resIndex) };
|
||||
#endif
|
||||
#ifdef Q_OS_LINUX
|
||||
constexpr int BUFFER_SIZE = 100;
|
||||
@@ -292,7 +307,7 @@ QString NetworkUtilities::getGatewayAndIface()
|
||||
|
||||
if ((sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)) < 0) {
|
||||
perror("socket failed");
|
||||
return "";
|
||||
return {};
|
||||
}
|
||||
|
||||
memset(msgbuf, 0, sizeof(msgbuf));
|
||||
@@ -316,7 +331,7 @@ QString NetworkUtilities::getGatewayAndIface()
|
||||
/* send msg */
|
||||
if (send(sock, nlmsg, nlmsg->nlmsg_len, 0) < 0) {
|
||||
perror("send failed");
|
||||
return "";
|
||||
return {};
|
||||
}
|
||||
|
||||
/* receive response */
|
||||
@@ -325,7 +340,7 @@ QString NetworkUtilities::getGatewayAndIface()
|
||||
received_bytes = recv(sock, ptr, sizeof(buffer) - msg_len, 0);
|
||||
if (received_bytes < 0) {
|
||||
perror("Error in recv");
|
||||
return "";
|
||||
return {};
|
||||
}
|
||||
|
||||
nlh = (struct nlmsghdr *) ptr;
|
||||
@@ -335,7 +350,7 @@ QString NetworkUtilities::getGatewayAndIface()
|
||||
(nlmsg->nlmsg_type == NLMSG_ERROR))
|
||||
{
|
||||
perror("Error in received packet");
|
||||
return "";
|
||||
return {};
|
||||
}
|
||||
|
||||
/* If we received all data break */
|
||||
@@ -388,10 +403,12 @@ QString NetworkUtilities::getGatewayAndIface()
|
||||
}
|
||||
}
|
||||
close(sock);
|
||||
return gateway_address;
|
||||
return { gateway_address, QNetworkInterface::interfaceFromName(interface) };
|
||||
#endif
|
||||
#if defined(Q_OS_MAC) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
||||
QString gateway;
|
||||
int index = -1;
|
||||
|
||||
int mib[] = {CTL_NET, PF_ROUTE, 0, 0, NET_RT_FLAGS, RTF_GATEWAY};
|
||||
int afinet_type[] = {AF_INET, AF_INET6};
|
||||
|
||||
@@ -401,17 +418,17 @@ QString NetworkUtilities::getGatewayAndIface()
|
||||
|
||||
size_t needed = 0;
|
||||
if (sysctl(mib, sizeof(mib) / sizeof(int), nullptr, &needed, nullptr, 0) < 0)
|
||||
return "";
|
||||
return {};
|
||||
|
||||
char* buf;
|
||||
if ((buf = new char[needed]) == 0)
|
||||
return "";
|
||||
return {};
|
||||
|
||||
if (sysctl(mib, sizeof(mib) / sizeof(int), buf, &needed, nullptr, 0) < 0)
|
||||
{
|
||||
qDebug() << "sysctl: net.route.0.0.dump";
|
||||
delete[] buf;
|
||||
return gateway;
|
||||
return {};
|
||||
}
|
||||
|
||||
struct rt_msghdr* rt;
|
||||
@@ -449,7 +466,10 @@ QString NetworkUtilities::getGatewayAndIface()
|
||||
&(reinterpret_cast<struct sockaddr_in*>(sa_tab[RTAX_GATEWAY]))->sin_addr,
|
||||
sizeof(struct in_addr));
|
||||
if (inet_ntop(AF_INET, srcStr4, dstStr4, INET_ADDRSTRLEN) != nullptr)
|
||||
{
|
||||
gateway = dstStr4;
|
||||
index = rt->rtm_index;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -463,7 +483,10 @@ QString NetworkUtilities::getGatewayAndIface()
|
||||
&(reinterpret_cast<struct sockaddr_in6*>(sa_tab[RTAX_GATEWAY]))->sin6_addr,
|
||||
sizeof(struct in6_addr));
|
||||
if (inet_ntop(AF_INET6, srcStr6, dstStr6, INET6_ADDRSTRLEN) != nullptr)
|
||||
{
|
||||
gateway = dstStr6;
|
||||
index = rt->rtm_index;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -472,6 +495,6 @@ QString NetworkUtilities::getGatewayAndIface()
|
||||
free(buf);
|
||||
}
|
||||
|
||||
return gateway;
|
||||
return { gateway, QNetworkInterface::interfaceFromIndex(index) };
|
||||
#endif
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
#include <QString>
|
||||
#include <QHostAddress>
|
||||
#include <QNetworkReply>
|
||||
|
||||
#include <QtNetwork/qnetworkinterface.h>
|
||||
|
||||
class NetworkUtilities : public QObject
|
||||
{
|
||||
@@ -17,7 +17,7 @@ public:
|
||||
static bool checkIPv4Format(const QString &ip);
|
||||
static bool checkIpSubnetFormat(const QString &ip);
|
||||
static bool checkIpv6Enabled();
|
||||
static QString getGatewayAndIface();
|
||||
static QPair<QString, QNetworkInterface> getGatewayAndIface();
|
||||
// Returns the Interface Index that could Route to dst
|
||||
static int AdapterIndexTo(const QHostAddress& dst);
|
||||
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
#include "osSignalHandler.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QMetaObject>
|
||||
#include <QSocketNotifier>
|
||||
|
||||
#include "../amnezia_application.h"
|
||||
|
||||
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
|
||||
#include <pthread.h>
|
||||
#include <signal.h>
|
||||
#include <sys/signalfd.h>
|
||||
#include <unistd.h>
|
||||
#elif defined(Q_OS_MACOS)
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#include <QAbstractNativeEventFilter>
|
||||
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
static bool initialized = false;
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
class WindowsCloseFilter : public QAbstractNativeEventFilter
|
||||
{
|
||||
public:
|
||||
bool nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result) override
|
||||
{
|
||||
MSG *msg = static_cast<MSG *>(message);
|
||||
|
||||
switch (msg->message) {
|
||||
case WM_CLOSE: {
|
||||
const HWND active = GetActiveWindow();
|
||||
const HWND self = msg->hwnd;
|
||||
if (active != self) {
|
||||
AmneziaApplication *app = qobject_cast<AmneziaApplication *>(QCoreApplication::instance());
|
||||
if (app) {
|
||||
QMetaObject::invokeMethod(app, "forceQuit", Qt::QueuedConnection);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
};
|
||||
|
||||
static WindowsCloseFilter *windowsFilter = nullptr;
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
|
||||
static int signalFd = -1;
|
||||
static QSocketNotifier *socketNotifier = nullptr;
|
||||
|
||||
static void setupUnixSignalHandler()
|
||||
{
|
||||
sigset_t set;
|
||||
sigemptyset(&set);
|
||||
sigaddset(&set, SIGINT);
|
||||
sigaddset(&set, SIGTERM);
|
||||
|
||||
pthread_sigmask(SIG_BLOCK, &set, nullptr);
|
||||
|
||||
signalFd = signalfd(-1, &set, SFD_NONBLOCK | SFD_CLOEXEC);
|
||||
if (signalFd < 0)
|
||||
return;
|
||||
|
||||
socketNotifier = new QSocketNotifier(signalFd, QSocketNotifier::Read, QCoreApplication::instance());
|
||||
|
||||
QObject::connect(socketNotifier, &QSocketNotifier::activated, QCoreApplication::instance(), [](int) {
|
||||
signalfd_siginfo fdsi;
|
||||
::read(signalFd, &fdsi, sizeof(fdsi));
|
||||
|
||||
if (fdsi.ssi_signo == SIGINT || fdsi.ssi_signo == SIGTERM) {
|
||||
QCoreApplication::quit();
|
||||
}
|
||||
});
|
||||
}
|
||||
#elif defined(Q_OS_MACOS)
|
||||
static int signalPipe[2] = { -1, -1 };
|
||||
static QSocketNotifier *socketNotifier = nullptr;
|
||||
|
||||
static void macSignalHandler(int)
|
||||
{
|
||||
if (signalPipe[1] >= 0) {
|
||||
const char ch = 1;
|
||||
::write(signalPipe[1], &ch, sizeof(ch));
|
||||
}
|
||||
}
|
||||
|
||||
static void setupUnixSignalHandler()
|
||||
{
|
||||
if (::pipe(signalPipe) != 0)
|
||||
return;
|
||||
|
||||
::fcntl(signalPipe[0], F_SETFL, O_NONBLOCK);
|
||||
::fcntl(signalPipe[1], F_SETFL, O_NONBLOCK);
|
||||
|
||||
socketNotifier = new QSocketNotifier(signalPipe[0], QSocketNotifier::Read, QCoreApplication::instance());
|
||||
|
||||
QObject::connect(socketNotifier, &QSocketNotifier::activated, QCoreApplication::instance(), [](int) {
|
||||
char buf[16];
|
||||
::read(signalPipe[0], buf, sizeof(buf));
|
||||
QCoreApplication::quit();
|
||||
});
|
||||
|
||||
struct sigaction sa {};
|
||||
sa.sa_handler = macSignalHandler;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
sa.sa_flags = 0;
|
||||
|
||||
sigaction(SIGINT, &sa, nullptr);
|
||||
sigaction(SIGTERM, &sa, nullptr);
|
||||
}
|
||||
#endif
|
||||
|
||||
static void cleanupUnixSignalHandler()
|
||||
{
|
||||
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
|
||||
if (socketNotifier) {
|
||||
socketNotifier->setEnabled(false);
|
||||
socketNotifier->deleteLater();
|
||||
socketNotifier = nullptr;
|
||||
}
|
||||
|
||||
if (signalFd >= 0) {
|
||||
::close(signalFd);
|
||||
signalFd = -1;
|
||||
}
|
||||
|
||||
#elif defined(Q_OS_MACOS)
|
||||
struct sigaction sa {};
|
||||
sa.sa_handler = SIG_DFL;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
sa.sa_flags = 0;
|
||||
sigaction(SIGINT, &sa, nullptr);
|
||||
sigaction(SIGTERM, &sa, nullptr);
|
||||
|
||||
if (socketNotifier) {
|
||||
socketNotifier->setEnabled(false);
|
||||
socketNotifier->deleteLater();
|
||||
socketNotifier = nullptr;
|
||||
}
|
||||
|
||||
if (signalPipe[0] >= 0) {
|
||||
::close(signalPipe[0]);
|
||||
signalPipe[0] = -1;
|
||||
}
|
||||
|
||||
if (signalPipe[1] >= 0) {
|
||||
::close(signalPipe[1]);
|
||||
signalPipe[1] = -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
if (windowsFilter) {
|
||||
QCoreApplication::instance()->removeNativeEventFilter(windowsFilter);
|
||||
delete windowsFilter;
|
||||
windowsFilter = nullptr;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
OsSignalHandler::OsSignalHandler(QObject *parent) : QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void OsSignalHandler::setup()
|
||||
{
|
||||
if (initialized)
|
||||
return;
|
||||
|
||||
initialized = true;
|
||||
|
||||
#if (defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)) || defined(Q_OS_MACOS)
|
||||
setupUnixSignalHandler();
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
windowsFilter = new WindowsCloseFilter();
|
||||
QCoreApplication::instance()->installNativeEventFilter(windowsFilter);
|
||||
#endif
|
||||
|
||||
QObject::connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, [] { cleanupUnixSignalHandler(); });
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#ifndef OSSIGNALHANDLER_H
|
||||
#define OSSIGNALHANDLER_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
class OsSignalHandler : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
static void setup();
|
||||
|
||||
private:
|
||||
explicit OsSignalHandler(QObject *parent = nullptr);
|
||||
static void handleSignal(int signal);
|
||||
};
|
||||
|
||||
#endif // OSSIGNALHANDLER_H
|
||||
@@ -11,7 +11,8 @@ QString amnezia::scriptFolder(amnezia::DockerContainer container)
|
||||
case DockerContainer::Cloak: return QLatin1String("openvpn_cloak");
|
||||
case DockerContainer::ShadowSocks: return QLatin1String("openvpn_shadowsocks");
|
||||
case DockerContainer::WireGuard: return QLatin1String("wireguard");
|
||||
case DockerContainer::Awg: return QLatin1String("awg");
|
||||
case DockerContainer::Awg2: return QLatin1String("awg");
|
||||
case DockerContainer::Awg: return QLatin1String("awg_legacy");
|
||||
case DockerContainer::Ipsec: return QLatin1String("ipsec");
|
||||
case DockerContainer::Xray: return QLatin1String("xray");
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace amnezia::serialization
|
||||
namespace vless
|
||||
{
|
||||
QJsonObject Deserialize(const QString &vless, QString *alias, QString *errMessage);
|
||||
const QString Serialize(const VlessServerObject &server, const QString &alias);
|
||||
} // namespace vless
|
||||
|
||||
namespace ss
|
||||
|
||||
@@ -42,6 +42,25 @@ struct VMessServerObject
|
||||
};
|
||||
|
||||
|
||||
struct VlessServerObject
|
||||
{
|
||||
QString address;
|
||||
QString id; // UUID
|
||||
int port;
|
||||
QString flow = "xtls-rprx-vision";
|
||||
QString encryption = "none";
|
||||
QString network = "tcp";
|
||||
QString security = "reality";
|
||||
QString serverName; // SNI
|
||||
QString publicKey;
|
||||
QString shortId;
|
||||
QString fingerprint = "chrome";
|
||||
QString spiderX = "";
|
||||
JSONSTRUCT_COMPARE(VlessServerObject, address, id, port, flow, encryption)
|
||||
JSONSTRUCT_REGISTER(VlessServerObject, F(address, id, port, flow, encryption, network, security, serverName, publicKey, shortId, fingerprint, spiderX))
|
||||
};
|
||||
|
||||
|
||||
namespace transfer
|
||||
{
|
||||
|
||||
|
||||
@@ -252,5 +252,65 @@ QJsonObject Deserialize(const QString &str, QString *alias, QString *errMessage)
|
||||
root["inbounds"] = QJsonArray { inbound };
|
||||
return root;
|
||||
}
|
||||
} // namespace amnezia::serialization::vless
|
||||
|
||||
const QString Serialize(const VlessServerObject &server, const QString &alias)
|
||||
{
|
||||
|
||||
QUrl url;
|
||||
|
||||
// Set basic URL components
|
||||
url.setScheme("vless");
|
||||
url.setUserInfo(server.id);
|
||||
url.setHost(server.address);
|
||||
url.setPort(server.port);
|
||||
|
||||
QUrlQuery query;
|
||||
|
||||
if (!server.network.isEmpty() && server.network != "tcp") {
|
||||
query.addQueryItem("type", server.network);
|
||||
}
|
||||
|
||||
if (!server.encryption.isEmpty()) {
|
||||
query.addQueryItem("encryption", server.encryption);
|
||||
}
|
||||
|
||||
if (!server.security.isEmpty() && server.security != "none") {
|
||||
query.addQueryItem("security", server.security);
|
||||
}
|
||||
|
||||
if (!server.flow.isEmpty() && (server.security == "xtls" || server.security == "reality")) {
|
||||
query.addQueryItem("flow", server.flow);
|
||||
}
|
||||
|
||||
if (!server.serverName.isEmpty()) {
|
||||
query.addQueryItem("sni", server.serverName);
|
||||
}
|
||||
|
||||
if (server.security == "reality") {
|
||||
if (!server.fingerprint.isEmpty()) {
|
||||
query.addQueryItem("fp", server.fingerprint);
|
||||
}
|
||||
|
||||
if (!server.publicKey.isEmpty()) {
|
||||
query.addQueryItem("pbk", server.publicKey);
|
||||
}
|
||||
|
||||
if (!server.shortId.isEmpty()) {
|
||||
query.addQueryItem("sid", server.shortId);
|
||||
}
|
||||
|
||||
if (!server.spiderX.isEmpty()) {
|
||||
query.addQueryItem("spiderX", server.spiderX);
|
||||
}
|
||||
}
|
||||
|
||||
url.setQuery(query);
|
||||
|
||||
if (!alias.isEmpty()) {
|
||||
url.setFragment(alias);
|
||||
}
|
||||
|
||||
return url.toString(QUrl::ComponentFormattingOption::FullyEncoded);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
#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
|
||||
@@ -0,0 +1,38 @@
|
||||
#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
|
||||
@@ -0,0 +1,354 @@
|
||||
#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
|
||||
@@ -0,0 +1,29 @@
|
||||
#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
|
||||
@@ -0,0 +1,817 @@
|
||||
#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
|
||||
@@ -0,0 +1,35 @@
|
||||
#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
|
||||
@@ -0,0 +1,157 @@
|
||||
#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
|
||||
@@ -0,0 +1,49 @@
|
||||
#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
|
||||
@@ -0,0 +1,345 @@
|
||||
#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
|
||||
@@ -0,0 +1,58 @@
|
||||
#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
|
||||
@@ -0,0 +1,36 @@
|
||||
#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
|
||||
@@ -440,18 +440,6 @@ bool Daemon::parseConfig(const QJsonObject& obj, InterfaceConfig& config) {
|
||||
if (!obj.value("I5").isNull()) {
|
||||
config.m_specialJunk["I5"] = obj.value("I5").toString();
|
||||
}
|
||||
if (!obj.value("J1").isNull()) {
|
||||
config.m_controlledJunk["J1"] = obj.value("J1").toString();
|
||||
}
|
||||
if (!obj.value("J2").isNull()) {
|
||||
config.m_controlledJunk["J2"] = obj.value("J2").toString();
|
||||
}
|
||||
if (!obj.value("J3").isNull()) {
|
||||
config.m_controlledJunk["J3"] = obj.value("J3").toString();
|
||||
}
|
||||
if (!obj.value("Itime").isNull()) {
|
||||
config.m_specialHandshakeTimeout = obj.value("Itime").toString();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -101,10 +101,10 @@ QString InterfaceConfig::toWgConf(const QMap<QString, QString>& extra) const {
|
||||
out << "MTU = " << m_deviceMTU << "\n";
|
||||
}
|
||||
|
||||
if (!m_primaryDnsServer.isNull()) {
|
||||
if (!m_primaryDnsServer.isEmpty()) {
|
||||
QStringList dnsServers;
|
||||
dnsServers.append(m_primaryDnsServer);
|
||||
if (!m_secondaryDnsServer.isNull()) {
|
||||
if (!m_secondaryDnsServer.isEmpty()) {
|
||||
dnsServers.append(m_secondaryDnsServer);
|
||||
}
|
||||
// If the DNS is not the Gateway, it's a user defined DNS
|
||||
@@ -152,12 +152,6 @@ QString InterfaceConfig::toWgConf(const QMap<QString, QString>& extra) const {
|
||||
for (const QString& key : m_specialJunk.keys()) {
|
||||
out << key << " = " << m_specialJunk[key] << "\n";
|
||||
}
|
||||
for (const QString& key : m_controlledJunk.keys()) {
|
||||
out << key << " = " << m_controlledJunk[key] << "\n";
|
||||
}
|
||||
if (!m_specialHandshakeTimeout.isNull()) {
|
||||
out << "Itime = " << m_specialHandshakeTimeout << "\n";
|
||||
}
|
||||
|
||||
// If any extra config was provided, append it now.
|
||||
for (const QString& key : extra.keys()) {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
|
||||
#include <QMap>
|
||||
#include "ipaddress.h"
|
||||
|
||||
class QJsonObject;
|
||||
@@ -57,8 +57,6 @@ class InterfaceConfig {
|
||||
QString m_underloadPacketMagicHeader;
|
||||
QString m_transportPacketMagicHeader;
|
||||
QMap<QString, QString> m_specialJunk;
|
||||
QMap<QString, QString> m_controlledJunk;
|
||||
QString m_specialHandshakeTimeout;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
QString toWgConf(
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<svg width="24" height="24" viewBox="0 0 74 74" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_4_34)">
|
||||
<path d="M55.5 12.3333H18.5C15.0942 12.3333 12.3333 15.0943 12.3333 18.5V55.5C12.3333 58.9058 15.0942 61.6667 18.5 61.6667H55.5C58.9057 61.6667 61.6666 58.9058 61.6666 55.5V18.5C61.6666 15.0943 58.9057 12.3333 55.5 12.3333Z" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M21.5833 24.6667H52.4167" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M21.5833 37H52.4167" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M21.5833 49.3333H40.0833" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="61.5" cy="12.5" r="15" fill="#FBB36B" stroke="#1C1D21" stroke-width="5"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_4_34">
|
||||
<rect width="74" height="74" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 982 B |
@@ -0,0 +1,8 @@
|
||||
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#CBCAC8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<!-- Основа газеты -->
|
||||
<rect x="4" y="4" width="16" height="16" rx="2"/>
|
||||
<!-- Линии текста -->
|
||||
<line x1="7" y1="8" x2="17" y2="8"/>
|
||||
<line x1="7" y1="12" x2="17" y2="12"/>
|
||||
<line x1="7" y1="16" x2="13" y2="16"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 410 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.0001 12C14.0001 10.8954 13.1047 10 12.0001 10C10.8955 10 10.0001 10.8954 10.0001 12C10.0001 13.1046 10.8955 14 12.0001 14C13.1047 14 14.0001 13.1046 14.0001 12ZM16.0001 12C16.0001 14.2091 14.2092 16 12.0001 16C9.79094 16 8.00008 14.2091 8.00008 12C8.00008 9.79086 9.79094 8 12.0001 8C14.2092 8 16.0001 9.79086 16.0001 12Z" fill="#D7D8DB"/>
|
||||
<path d="M20.5001 6C21.8808 6 23.0001 4.88071 23.0001 3.5C23.0001 2.11929 21.8808 1 20.5001 1C19.1194 1 18.0001 2.11929 18.0001 3.5C18.0001 4.88071 19.1194 6 20.5001 6Z" fill="#FBB36A"/>
|
||||
<path d="M12.2198 1C12.9657 1 13.6811 1.28098 14.2315 1.7793C14.0814 2.32745 14.0001 2.90421 14.0001 3.5C14.0001 4.75704 14.3578 5.93021 14.9757 6.9248L14.7198 6.77539C14.2644 6.51244 13.8864 6.13489 13.6231 5.67969C13.3599 5.22423 13.2203 4.70672 13.2198 4.18066V4C13.2198 3.73478 13.1144 3.48051 12.9268 3.29297C12.7393 3.10557 12.4849 3 12.2198 3H11.7803C11.5153 3.00005 11.2608 3.10556 11.0733 3.29297C10.8858 3.4805 10.7803 3.73482 10.7803 4V4.18066C10.7798 4.70675 10.6403 5.22422 10.377 5.67969C10.1138 6.1349 9.73575 6.51343 9.28035 6.77637L9.27937 6.77539L8.85261 7.02441L8.84968 7.02637C8.39376 7.28946 7.87607 7.42773 7.34968 7.42773C6.82327 7.42764 6.30558 7.28958 5.84968 7.02637L5.72956 6.96191L5.70027 6.94629C5.47084 6.81401 5.1983 6.77827 4.94245 6.84668C4.71846 6.90662 4.52295 7.04188 4.38874 7.22852L4.33503 7.31152L4.11531 7.69141C3.98385 7.92039 3.94855 8.19212 4.01667 8.44727C4.07655 8.6712 4.21199 8.86674 4.39851 9.00098L4.48152 9.05469L4.53523 9.08789L4.68464 9.1875C5.11379 9.44747 5.47116 9.8098 5.7237 10.2441C5.98685 10.6968 6.12679 11.2108 6.12995 11.7344V12.2461L6.1241 12.4434C6.09561 12.9034 5.96204 13.3518 5.73152 13.7529C5.47084 14.2065 5.09507 14.5827 4.64363 14.8467L4.6446 14.8477L4.49421 14.9375L4.48152 14.9453C4.25235 15.0781 4.08509 15.2969 4.01667 15.5527C3.94855 15.8079 3.98385 16.0796 4.11531 16.3086L4.33406 16.6885C4.4668 16.9177 4.68662 17.0849 4.94245 17.1533C5.1983 17.2217 5.47084 17.186 5.70027 17.0537L5.72956 17.0381L5.84968 16.9736L6.02351 16.8809C6.43513 16.678 6.88902 16.5724 7.34968 16.5723C7.81025 16.5723 8.26427 16.6781 8.67585 16.8809L8.84968 16.9736L8.85261 16.9756L9.27937 17.2236C9.73477 17.4866 10.1138 17.8651 10.377 18.3203C10.6403 18.7758 10.7798 19.2932 10.7803 19.8193V20C10.7803 20.2652 10.8858 20.5195 11.0733 20.707C11.2608 20.8944 11.5153 21 11.7803 21H12.2198C12.4849 21 12.7393 20.8944 12.9268 20.707C13.1144 20.5195 13.2198 20.2652 13.2198 20V19.8193L13.2266 19.6221C13.2572 19.1646 13.3928 18.7188 13.6231 18.3203C13.8534 17.9221 14.1718 17.5828 14.5528 17.3281L14.7198 17.2236L15.1475 16.9756L15.1505 16.9736L15.3243 16.8809C15.7359 16.6781 16.1899 16.5723 16.6505 16.5723C17.1769 16.5723 17.6946 16.7104 18.1505 16.9736L18.2999 17.0537C18.5293 17.186 18.8018 17.2217 19.0577 17.1533C19.3136 17.0849 19.5323 16.9177 19.6651 16.6885L19.879 16.3086L19.8839 16.3008L19.9288 16.2119C20.0233 16.0026 20.0433 15.7658 19.9835 15.542C19.915 15.2863 19.7477 15.0683 19.5186 14.9355L19.3995 14.8721C19.3898 14.8669 19.3797 14.862 19.3702 14.8564C18.9124 14.5921 18.532 14.2112 18.2686 13.7529C18.0053 13.2946 17.8681 12.7746 17.8702 12.2461V11.75C17.8688 11.2228 18.0059 10.7042 18.2686 10.2471C18.3758 10.0606 18.5033 9.88765 18.6466 9.72949C19.2341 9.90406 19.8558 10 20.5001 10C20.8249 10 21.144 9.97473 21.4561 9.92871C21.2126 10.2693 20.9013 10.5607 20.5333 10.7764L20.5343 10.7773L20.3702 10.876C20.2176 10.9641 20.0908 11.0914 20.003 11.2441C19.9153 11.3968 19.8695 11.57 19.8702 11.7461V12.2539C19.8695 12.43 19.9153 12.6032 20.003 12.7559C20.0894 12.9062 20.214 13.0312 20.3634 13.1191L20.4903 13.1875L20.5216 13.2051L20.7706 13.3662C21.3298 13.7689 21.7365 14.3539 21.9161 15.0254C22.1205 15.7898 22.0131 16.6035 21.6202 17.29L21.6212 17.291L21.4014 17.6816L21.3956 17.6914C20.9972 18.379 20.342 18.8807 19.5743 19.0859C18.817 19.2883 18.0116 19.1847 17.3292 18.8008V18.8027L17.1798 18.7227C17.17 18.7175 17.16 18.7116 17.1505 18.7061C16.9986 18.6184 16.8258 18.5723 16.6505 18.5723C16.4751 18.5723 16.3024 18.6184 16.1505 18.7061L16.1495 18.7051L15.7227 18.9541L15.6114 19.0283C15.5077 19.1078 15.4204 19.2075 15.3546 19.3213C15.2668 19.4731 15.22 19.646 15.2198 19.8213V20C15.2198 20.7955 14.9043 21.5585 14.3419 22.1211C13.7793 22.6836 13.0154 23 12.2198 23H11.7803C10.9848 23 10.2208 22.6837 9.65828 22.1211C9.09594 21.5585 8.78035 20.7954 8.78035 20V19.8213L8.77156 19.6904C8.75435 19.5609 8.71141 19.4352 8.64558 19.3213C8.57971 19.2075 8.49243 19.1078 8.38874 19.0283L8.27742 18.9541L7.84968 18.7051C7.69776 18.6175 7.52506 18.5723 7.34968 18.5723C7.21824 18.5723 7.08852 18.5985 6.96785 18.6484L6.84968 18.7061C6.84011 18.7116 6.83013 18.7175 6.82038 18.7227L6.67097 18.8027L6.66999 18.8008C5.98771 19.1844 5.18286 19.2883 4.42585 19.0859C3.75418 18.9063 3.16836 18.4998 2.7657 17.9404L2.60456 17.6914L2.38484 17.3105L2.38386 17.3096C1.98692 16.6214 1.87897 15.8037 2.08406 15.0361C2.28803 14.2733 2.78502 13.6217 3.46589 13.2227L3.62995 13.124L3.73933 13.0508C3.84344 12.9708 3.93128 12.8704 3.99714 12.7559C4.08484 12.6032 4.13065 12.43 4.12995 12.2539V11.7461L4.12117 11.6162C4.10349 11.4873 4.06003 11.3622 3.99421 11.249C3.92841 11.136 3.84152 11.037 3.73835 10.958L3.62995 10.8857L3.57527 10.8516L3.42585 10.752C2.76614 10.351 2.2839 9.71122 2.08406 8.96387C1.87897 8.19633 1.98692 7.37864 2.38386 6.69043L2.38484 6.68848L2.60456 6.30859L2.7657 6.05957C3.16836 5.50016 3.75418 5.09371 4.42585 4.91406C5.1827 4.71172 5.9878 4.81485 6.66999 5.19824L6.67097 5.19727L6.82038 5.27734L6.84968 5.29395L6.967Line truncated
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.9 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 35 35" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="17.5" cy="17.5" r="15" fill="#FBB36B" stroke="#1C1D21" stroke-width="5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 188 B |
@@ -32,17 +32,41 @@
|
||||
<false/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>AmneziaVPNLaunchScreen</string>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<true/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneClassName</key>
|
||||
<string>UIWindowScene</string>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>Default Configuration</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>QIOSWindowSceneDelegate</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array/>
|
||||
<key>UIRequiresFullScreen</key>
|
||||
<true/>
|
||||
<false/>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array/>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Light</string>
|
||||
<key>com.wireguard.ios.app_group_id</key>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <QTimer>
|
||||
|
||||
#include "amnezia_application.h"
|
||||
#include "core/osSignalHandler.h"
|
||||
#include "migrations.h"
|
||||
#include "version.h"
|
||||
|
||||
@@ -44,6 +45,7 @@ int main(int argc, char *argv[])
|
||||
#endif
|
||||
|
||||
AmneziaApplication app(argc, argv);
|
||||
OsSignalHandler::setup();
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
||||
if (isAnotherInstanceRunning()) {
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QHostAddress>
|
||||
@@ -12,12 +15,13 @@
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QLocalSocket>
|
||||
#include <QObject>
|
||||
#include <QStandardPaths>
|
||||
#include <QTimer>
|
||||
|
||||
#include "ipaddress.h"
|
||||
#include "leakdetector.h"
|
||||
#include "logger.h"
|
||||
#include "models/server.h"
|
||||
#include "daemon/daemonerrors.h"
|
||||
|
||||
#include "protocols/protocols_defs.h"
|
||||
@@ -115,7 +119,6 @@ void LocalSocketController::daemonConnected() {
|
||||
}
|
||||
|
||||
void LocalSocketController::activate(const QJsonObject &rawConfig) {
|
||||
|
||||
QString protocolName = rawConfig.value("protocol").toString();
|
||||
|
||||
int splitTunnelType = rawConfig.value("splitTunnelType").toInt();
|
||||
@@ -132,13 +135,16 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
|
||||
// json.insert("hopindex", QJsonValue((double)hop.m_hopindex));
|
||||
json.insert("privateKey", wgConfig.value(amnezia::config_key::client_priv_key));
|
||||
json.insert("deviceIpv4Address", wgConfig.value(amnezia::config_key::client_ip));
|
||||
m_deviceIpv4 = wgConfig.value(amnezia::config_key::client_ip).toString();
|
||||
|
||||
// set up IPv6 unique-local-address, ULA, with "fd00::/8" prefix, not globally routable.
|
||||
// this will be default IPv6 gateway, OS recognizes that IPv6 link is local and switches to IPv4.
|
||||
// Otherwise some OSes (Linux) try IPv6 forever and hang.
|
||||
// https://en.wikipedia.org/wiki/Unique_local_address (RFC 4193)
|
||||
// https://man7.org/linux/man-pages/man5/gai.conf.5.html
|
||||
json.insert("deviceIpv6Address", "fd58:baa6:dead::1"); // simply "dead::1" is globally-routable, don't use it
|
||||
|
||||
// simply "dead::1" is globally-routable, don't use it
|
||||
json.insert("deviceIpv6Address", "fd58:baa6:dead::1");
|
||||
|
||||
json.insert("serverPublicKey", wgConfig.value(amnezia::config_key::server_pub_key));
|
||||
json.insert("serverPskKey", wgConfig.value(amnezia::config_key::psk_key));
|
||||
@@ -220,7 +226,6 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
|
||||
|
||||
json.insert("allowedIPAddressRanges", jsAllowedIPAddesses);
|
||||
|
||||
|
||||
QJsonArray jsExcludedAddresses;
|
||||
jsExcludedAddresses.append(wgConfig.value(amnezia::config_key::hostName));
|
||||
if (splitTunnelType == 2) {
|
||||
@@ -255,10 +260,6 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
|
||||
json.insert(amnezia::config_key::specialJunk3, wgConfig.value(amnezia::config_key::specialJunk3));
|
||||
json.insert(amnezia::config_key::specialJunk4, wgConfig.value(amnezia::config_key::specialJunk4));
|
||||
json.insert(amnezia::config_key::specialJunk5, wgConfig.value(amnezia::config_key::specialJunk5));
|
||||
json.insert(amnezia::config_key::controlledJunk1, wgConfig.value(amnezia::config_key::controlledJunk1));
|
||||
json.insert(amnezia::config_key::controlledJunk2, wgConfig.value(amnezia::config_key::controlledJunk2));
|
||||
json.insert(amnezia::config_key::controlledJunk3, wgConfig.value(amnezia::config_key::controlledJunk3));
|
||||
json.insert(amnezia::config_key::specialHandshakeTimeout, wgConfig.value(amnezia::config_key::specialHandshakeTimeout));
|
||||
} else if (!wgConfig.value(amnezia::config_key::junkPacketCount).isUndefined()
|
||||
&& !wgConfig.value(amnezia::config_key::junkPacketMinSize).isUndefined()
|
||||
&& !wgConfig.value(amnezia::config_key::junkPacketMaxSize).isUndefined()
|
||||
@@ -274,11 +275,7 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
|
||||
&& !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::controlledJunk1).isUndefined()
|
||||
&& !wgConfig.value(amnezia::config_key::controlledJunk2).isUndefined()
|
||||
&& !wgConfig.value(amnezia::config_key::controlledJunk3).isUndefined()
|
||||
&& !wgConfig.value(amnezia::config_key::specialHandshakeTimeout).isUndefined()) {
|
||||
&& !wgConfig.value(amnezia::config_key::specialJunk5).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));
|
||||
@@ -295,10 +292,6 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
|
||||
json.insert(amnezia::config_key::specialJunk3, wgConfig.value(amnezia::config_key::specialJunk3));
|
||||
json.insert(amnezia::config_key::specialJunk4, wgConfig.value(amnezia::config_key::specialJunk4));
|
||||
json.insert(amnezia::config_key::specialJunk5, wgConfig.value(amnezia::config_key::specialJunk5));
|
||||
json.insert(amnezia::config_key::controlledJunk1, wgConfig.value(amnezia::config_key::controlledJunk1));
|
||||
json.insert(amnezia::config_key::controlledJunk2, wgConfig.value(amnezia::config_key::controlledJunk2));
|
||||
json.insert(amnezia::config_key::controlledJunk3, wgConfig.value(amnezia::config_key::controlledJunk3));
|
||||
json.insert(amnezia::config_key::specialHandshakeTimeout, wgConfig.value(amnezia::config_key::specialHandshakeTimeout));
|
||||
}
|
||||
|
||||
write(json);
|
||||
@@ -449,6 +442,7 @@ void LocalSocketController::parseCommand(const QByteArray& command) {
|
||||
}
|
||||
|
||||
if (type == "status") {
|
||||
|
||||
QJsonValue serverIpv4Gateway = obj.value("serverIpv4Gateway");
|
||||
if (!serverIpv4Gateway.isString()) {
|
||||
logger.error() << "Unexpected serverIpv4Gateway value";
|
||||
@@ -493,6 +487,11 @@ void LocalSocketController::parseCommand(const QByteArray& command) {
|
||||
|
||||
logger.debug() << "Handshake completed with:"
|
||||
<< pubkey.toString();
|
||||
|
||||
checkStatus();
|
||||
|
||||
emit statusUpdated("", m_deviceIpv4, 0, 0);
|
||||
|
||||
emit connected(pubkey.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
#include "controllerimpl.h"
|
||||
|
||||
|
||||
class QJsonObject;
|
||||
|
||||
class LocalSocketController final : public ControllerImpl {
|
||||
@@ -58,6 +59,7 @@ class LocalSocketController final : public ControllerImpl {
|
||||
|
||||
QByteArray m_buffer;
|
||||
|
||||
QString m_deviceIpv4;
|
||||
std::function<void(const QString&)> m_logCallback = nullptr;
|
||||
|
||||
QTimer m_initializingTimer;
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include "logger.h"
|
||||
//#include "mozillavpn.h"
|
||||
#include "networkwatcherimpl.h"
|
||||
#include "platforms/dummy/dummynetworkwatcher.h"
|
||||
//#include "settingsholder.h"
|
||||
|
||||
#ifdef MZ_WINDOWS
|
||||
@@ -51,7 +50,7 @@ NetworkWatcher::NetworkWatcher() { MZ_COUNT_CTOR(NetworkWatcher); }
|
||||
NetworkWatcher::~NetworkWatcher() { MZ_COUNT_DTOR(NetworkWatcher); }
|
||||
|
||||
void NetworkWatcher::initialize() {
|
||||
logger.debug() << "Initialize";
|
||||
logger.debug() << "Initialize NetworkWatcher";
|
||||
|
||||
#if defined(MZ_WINDOWS)
|
||||
m_impl = new WindowsNetworkWatcher(this);
|
||||
@@ -69,59 +68,45 @@ void NetworkWatcher::initialize() {
|
||||
m_impl = new DummyNetworkWatcher(this);
|
||||
#endif
|
||||
|
||||
|
||||
connect(m_impl, &NetworkWatcherImpl::unsecuredNetwork, this,
|
||||
&NetworkWatcher::unsecuredNetwork);
|
||||
connect(m_impl, &NetworkWatcherImpl::networkChanged, this,
|
||||
&NetworkWatcher::networkChange);
|
||||
|
||||
connect(m_impl, &NetworkWatcherImpl::sleepMode, this,
|
||||
&NetworkWatcher::onSleepMode);
|
||||
m_impl->initialize();
|
||||
|
||||
|
||||
// TODO: IMPL FOR AMNEZIA
|
||||
#if 0
|
||||
SettingsHolder* settingsHolder = SettingsHolder::instance();
|
||||
Q_ASSERT(settingsHolder);
|
||||
|
||||
m_active = settingsHolder->unsecuredNetworkAlert() ||
|
||||
settingsHolder->captivePortalAlert();
|
||||
m_reportUnsecuredNetwork = settingsHolder->unsecuredNetworkAlert();
|
||||
if (m_active) {
|
||||
// Enable sleep/wake monitoring for VPN auto-reconnection
|
||||
logger.debug() << "Starting NetworkWatcher for sleep/wake monitoring";
|
||||
logger.debug() << "About to call m_impl->start()";
|
||||
try {
|
||||
m_impl->start();
|
||||
logger.debug() << "m_impl->start() completed successfully";
|
||||
} catch (const std::exception& e) {
|
||||
logger.error() << "Exception in m_impl->start():" << e.what();
|
||||
} catch (...) {
|
||||
logger.error() << "Unknown exception in m_impl->start()";
|
||||
}
|
||||
|
||||
connect(settingsHolder, &SettingsHolder::unsecuredNetworkAlertChanged, this,
|
||||
&NetworkWatcher::settingsChanged);
|
||||
connect(settingsHolder, &SettingsHolder::captivePortalAlertChanged, this,
|
||||
&NetworkWatcher::settingsChanged);
|
||||
|
||||
#endif
|
||||
m_active = true;
|
||||
m_reportUnsecuredNetwork = false; // Disable unsecured network alerts for Amnezia
|
||||
}
|
||||
|
||||
void NetworkWatcher::settingsChanged() {
|
||||
// TODO: IMPL FOR AMNEZIA
|
||||
#if 0
|
||||
SettingsHolder* settingsHolder = SettingsHolder::instance();
|
||||
m_active = settingsHolder->unsecuredNetworkAlert() ||
|
||||
settingsHolder->captivePortalAlert();
|
||||
m_reportUnsecuredNetwork = settingsHolder->unsecuredNetworkAlert();
|
||||
// For Amnezia: Keep NetworkWatcher always active for sleep/wake monitoring
|
||||
logger.debug() << "NetworkWatcher settings changed - keeping sleep monitoring active";
|
||||
}
|
||||
|
||||
if (m_active) {
|
||||
logger.debug()
|
||||
<< "Starting Network Watcher; Reporting of Unsecured Networks: "
|
||||
<< m_reportUnsecuredNetwork;
|
||||
m_impl->start();
|
||||
} else {
|
||||
logger.debug() << "Stopping Network Watcher";
|
||||
m_impl->stop();
|
||||
}
|
||||
#endif
|
||||
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)
|
||||
<< "id:" << logger.sensitive(networkId);
|
||||
|
||||
#ifndef UNIT_TEST
|
||||
if (!m_reportUnsecuredNetwork) {
|
||||
logger.debug() << "Disabled. Ignoring unsecured network";
|
||||
|
||||
@@ -29,10 +29,13 @@ public:
|
||||
// false to restore.
|
||||
void simulateDisconnection(bool simulatedDisconnection);
|
||||
|
||||
void onSleepMode();
|
||||
|
||||
QNetworkInformation::Reachability getReachability();
|
||||
|
||||
signals:
|
||||
void networkChange();
|
||||
void sleepMode();
|
||||
|
||||
private:
|
||||
void settingsChanged();
|
||||
|
||||
@@ -41,6 +41,8 @@ signals:
|
||||
// TODO: Only windows-networkwatcher has this, the other plattforms should
|
||||
// too.
|
||||
void networkChanged(QString newBSSID);
|
||||
void sleepMode();
|
||||
|
||||
|
||||
private:
|
||||
bool m_active = false;
|
||||
|
||||
@@ -41,6 +41,7 @@ void PingHelper::start(const QString& serverIpv4Gateway,
|
||||
|
||||
m_gateway = QHostAddress(serverIpv4Gateway);
|
||||
m_source = QHostAddress(deviceIpv4Address.section('/', 0, 0));
|
||||
|
||||
m_pingSender = PingSenderFactory::create(m_source, this);
|
||||
|
||||
// Some platforms require root access to send and receive ICMP pings. If
|
||||
@@ -53,8 +54,10 @@ void PingHelper::start(const QString& serverIpv4Gateway,
|
||||
|
||||
connect(m_pingSender, &PingSender::recvPing, this, &PingHelper::pingReceived,
|
||||
Qt::QueuedConnection);
|
||||
connect(m_pingSender, &PingSender::criticalPingError, this,
|
||||
[]() { logger.info() << "Encountered Unrecoverable ping error"; });
|
||||
connect(m_pingSender, &PingSender::criticalPingError, this, [this]() {
|
||||
logger.info() << "Encountered Unrecoverable ping error";
|
||||
emit connectionLose();
|
||||
});
|
||||
|
||||
// Reset the ping statistics
|
||||
m_sequence = 0;
|
||||
|
||||
@@ -33,6 +33,8 @@ class PingHelper final : public QObject {
|
||||
|
||||
signals:
|
||||
void pingSentAndReceived(qint64 msec);
|
||||
void connectionLose();
|
||||
|
||||
|
||||
private:
|
||||
void nextPing();
|
||||
|
||||
@@ -5,27 +5,26 @@
|
||||
#include "pingsenderfactory.h"
|
||||
|
||||
#if defined(MZ_LINUX) || defined(MZ_ANDROID)
|
||||
//# include "platforms/linux/linuxpingsender.h"
|
||||
# include "platforms/linux/linuxpingsender.h"
|
||||
#elif defined(MZ_MACOS) || defined(MZ_IOS)
|
||||
# include "platforms/macos/macospingsender.h"
|
||||
# include "platforms/macos/macospingsender.h"
|
||||
#elif defined(MZ_WINDOWS)
|
||||
# include "platforms/windows/windowspingsender.h"
|
||||
#elif defined(MZ_DUMMY) || defined(UNIT_TEST)
|
||||
# include "platforms/dummy/dummypingsender.h"
|
||||
# include "platforms/windows/windowspingsender.h"
|
||||
#elif defined(MZ_WASM) || defined(UNIT_TEST)
|
||||
# include "platforms/dummy/dummypingsender.h"
|
||||
#else
|
||||
# error "Unsupported platform"
|
||||
# error "Unsupported platform"
|
||||
#endif
|
||||
|
||||
PingSender* PingSenderFactory::create(const QHostAddress& source,
|
||||
QObject* parent) {
|
||||
#if defined(MZ_LINUX) || defined(MZ_ANDROID)
|
||||
return nullptr;
|
||||
// return new LinuxPingSender(source, parent);
|
||||
return new LinuxPingSender(source, parent);
|
||||
#elif defined(MZ_MACOS) || defined(MZ_IOS)
|
||||
return new MacOSPingSender(source, parent);
|
||||
return new MacOSPingSender(source, parent);
|
||||
#elif defined(MZ_WINDOWS)
|
||||
return new WindowsPingSender(source, parent);
|
||||
return new WindowsPingSender(source, parent);
|
||||
#else
|
||||
return new DummyPingSender(source, parent);
|
||||
return new DummyPingSender(source, parent);
|
||||
#endif
|
||||
}
|
||||
@@ -10,9 +10,10 @@ class QHostAddress;
|
||||
class QObject;
|
||||
|
||||
class PingSenderFactory final {
|
||||
public:
|
||||
PingSenderFactory() = delete;
|
||||
static PingSender* create(const QHostAddress& source, QObject* parent);
|
||||
public:
|
||||
PingSenderFactory() = delete;
|
||||
static PingSender* create(const QHostAddress& source, QObject* parent);
|
||||
};
|
||||
|
||||
|
||||
#endif // PINGSENDERFACTORY_H
|
||||
@@ -99,7 +99,9 @@ bool AndroidController::initialize()
|
||||
{"onFileOpened", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onFileOpened)},
|
||||
{"onConfigImported", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onConfigImported)},
|
||||
{"onAuthResult", "(Z)V", reinterpret_cast<void *>(onAuthResult)},
|
||||
{"decodeQrCode", "(Ljava/lang/String;)Z", reinterpret_cast<bool *>(decodeQrCode)}
|
||||
{"decodeQrCode", "(Ljava/lang/String;)Z", reinterpret_cast<bool *>(decodeQrCode)},
|
||||
{"onImeInsetsChanged", "(I)V", reinterpret_cast<void *>(onImeInsetsChanged)},
|
||||
{"onSystemBarsInsetsChanged", "(II)V", reinterpret_cast<void *>(onSystemBarsInsetsChanged)}
|
||||
};
|
||||
|
||||
QJniEnvironment env;
|
||||
@@ -202,6 +204,21 @@ bool AndroidController::isOnTv()
|
||||
return callActivityMethod<jboolean>("isOnTv", "()Z");
|
||||
}
|
||||
|
||||
bool AndroidController::isEdgeToEdgeEnabled()
|
||||
{
|
||||
return callActivityMethod<jboolean>("isEdgeToEdgeEnabled", "()Z");
|
||||
}
|
||||
|
||||
int AndroidController::getStatusBarHeight()
|
||||
{
|
||||
return callActivityMethod<jint>("getStatusBarHeight", "()I");
|
||||
}
|
||||
|
||||
int AndroidController::getNavigationBarHeight()
|
||||
{
|
||||
return callActivityMethod<jint>("getNavigationBarHeight", "()I");
|
||||
}
|
||||
|
||||
void AndroidController::startQrReaderActivity()
|
||||
{
|
||||
callActivityMethod("startQrCodeReader", "()V");
|
||||
@@ -521,3 +538,23 @@ bool AndroidController::decodeQrCode(JNIEnv *env, jobject thiz, jstring data)
|
||||
|
||||
return ImportController::decodeQrCode(AndroidUtils::convertJString(env, data));
|
||||
}
|
||||
// static
|
||||
void AndroidController::onImeInsetsChanged(JNIEnv *env, jobject thiz, jint heightDp)
|
||||
{
|
||||
Q_UNUSED(env);
|
||||
Q_UNUSED(thiz);
|
||||
|
||||
qDebug() << "Android IME insets changed: height =" << heightDp << "dp";
|
||||
emit AndroidController::instance()->imeInsetsChanged(heightDp);
|
||||
}
|
||||
|
||||
// static
|
||||
void AndroidController::onSystemBarsInsetsChanged(JNIEnv *env, jobject thiz, jint navBarHeightDp, jint statusBarHeightDp)
|
||||
{
|
||||
Q_UNUSED(env);
|
||||
Q_UNUSED(thiz);
|
||||
|
||||
qDebug() << "Android system bars insets changed: nav bar =" << navBarHeightDp << "dp, status bar =" << statusBarHeightDp << "dp";
|
||||
emit AndroidController::instance()->systemBarsInsetsChanged(navBarHeightDp, statusBarHeightDp);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,9 @@ public:
|
||||
QString getFileName(const QString &uri);
|
||||
bool isCameraPresent();
|
||||
bool isOnTv();
|
||||
bool isEdgeToEdgeEnabled();
|
||||
int getStatusBarHeight();
|
||||
int getNavigationBarHeight();
|
||||
void startQrReaderActivity();
|
||||
void setSaveLogs(bool enabled);
|
||||
void exportLogsFile(const QString &fileName);
|
||||
@@ -70,6 +73,8 @@ signals:
|
||||
void importConfigFromOutside(QString config);
|
||||
void initConnectionState(Vpn::ConnectionState state);
|
||||
void authenticationResult(bool result);
|
||||
void imeInsetsChanged(int heightDp);
|
||||
void systemBarsInsetsChanged(int navBarHeightDp, int statusBarHeightDp);
|
||||
|
||||
private:
|
||||
bool isWaitingStatus = true;
|
||||
@@ -98,6 +103,8 @@ private:
|
||||
static void onFileOpened(JNIEnv *env, jobject thiz, jstring uri);
|
||||
static void onAuthResult(JNIEnv *env, jobject thiz, jboolean result);
|
||||
static bool decodeQrCode(JNIEnv *env, jobject thiz, jstring data);
|
||||
static void onImeInsetsChanged(JNIEnv *env, jobject thiz, jint heightDp);
|
||||
static void onSystemBarsInsetsChanged(JNIEnv *env, jobject thiz, jint navBarHeightDp, jint statusBarHeightDp);
|
||||
|
||||
template <typename Ret, typename ...Args>
|
||||
static auto callActivityMethod(const char *methodName, const char *signature, Args &&...args);
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <objc/runtime.h>
|
||||
#include <dispatch/dispatch.h>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QFile>
|
||||
#include <QString>
|
||||
|
||||
#include "ios_controller.h"
|
||||
|
||||
using SceneOpenURLContexts = void (*)(id, SEL, UIScene *, NSSet<UIOpenURLContext *> *);
|
||||
|
||||
static SceneOpenURLContexts g_originalSceneOpenURLContexts = nullptr;
|
||||
|
||||
static void amnezia_handleURL(NSURL *url)
|
||||
{
|
||||
if (!url || !url.isFileURL) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString filePath(url.path.UTF8String);
|
||||
if (filePath.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
if (filePath.contains("backup")) {
|
||||
IosController::Instance()->importBackupFromOutside(filePath);
|
||||
return;
|
||||
}
|
||||
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QByteArray data = file.readAll();
|
||||
IosController::Instance()->importConfigFromOutside(QString::fromUtf8(data));
|
||||
});
|
||||
}
|
||||
|
||||
static void amnezia_scene_openURLContexts(id self, SEL _cmd, UIScene *scene, NSSet<UIOpenURLContext *> *contexts)
|
||||
{
|
||||
if (g_originalSceneOpenURLContexts) {
|
||||
g_originalSceneOpenURLContexts(self, _cmd, scene, contexts);
|
||||
}
|
||||
|
||||
if (!contexts || contexts.count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (@available(iOS 13.0, *)) {
|
||||
for (UIOpenURLContext *context in contexts) {
|
||||
amnezia_handleURL(context.URL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@interface AmneziaSceneDelegateHooks : NSObject
|
||||
@end
|
||||
|
||||
@implementation AmneziaSceneDelegateHooks
|
||||
|
||||
+ (void)load
|
||||
{
|
||||
Class cls = objc_getClass("QIOSWindowSceneDelegate");
|
||||
if (!cls) {
|
||||
return;
|
||||
}
|
||||
|
||||
SEL selector = @selector(scene:openURLContexts:);
|
||||
Method method = class_getInstanceMethod(cls, selector);
|
||||
if (method) {
|
||||
g_originalSceneOpenURLContexts = reinterpret_cast<SceneOpenURLContexts>(method_getImplementation(method));
|
||||
method_setImplementation(method, reinterpret_cast<IMP>(amnezia_scene_openURLContexts));
|
||||
} else {
|
||||
const char *types = "v@:@@";
|
||||
class_addMethod(cls, selector, reinterpret_cast<IMP>(amnezia_scene_openURLContexts), types);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -2,7 +2,8 @@ import Foundation
|
||||
import os.log
|
||||
|
||||
struct Log {
|
||||
static let osLog = Logger()
|
||||
private static let subsystemIdentifier = Bundle.main.bundleIdentifier ?? "org.amnezia.AmneziaVPN"
|
||||
static let osLog = Logger(subsystem: subsystemIdentifier, category: "App")
|
||||
|
||||
private static let IsLoggingEnabledKey = "IsLoggingEnabled"
|
||||
static var isLoggingEnabled: Bool {
|
||||
@@ -77,9 +78,40 @@ struct Log {
|
||||
static func log(_ type: OSLogType, title: String = "", message: String, url: URL = neLogURL) {
|
||||
NSLog("\(title) \(message)")
|
||||
|
||||
guard isLoggingEnabled else { return }
|
||||
switch type {
|
||||
case .debug:
|
||||
if title.isEmpty {
|
||||
osLog.debug("\(message, privacy: .public)")
|
||||
} else {
|
||||
osLog.debug("\(title, privacy: .public) \(message, privacy: .public)")
|
||||
}
|
||||
case .info:
|
||||
if title.isEmpty {
|
||||
osLog.info("\(message, privacy: .public)")
|
||||
} else {
|
||||
osLog.info("\(title, privacy: .public) \(message, privacy: .public)")
|
||||
}
|
||||
case .error:
|
||||
if title.isEmpty {
|
||||
osLog.error("\(message, privacy: .public)")
|
||||
} else {
|
||||
osLog.error("\(title, privacy: .public) \(message, privacy: .public)")
|
||||
}
|
||||
case .fault:
|
||||
if title.isEmpty {
|
||||
osLog.fault("\(message, privacy: .public)")
|
||||
} else {
|
||||
osLog.fault("\(title, privacy: .public) \(message, privacy: .public)")
|
||||
}
|
||||
default:
|
||||
if title.isEmpty {
|
||||
osLog.log("\(message, privacy: .public)")
|
||||
} else {
|
||||
osLog.log("\(title, privacy: .public) \(message, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
osLog.log(level: type, "\(title) \(message)")
|
||||
guard isLoggingEnabled else { return }
|
||||
|
||||
let date = Date()
|
||||
let level = Record.Level(from: type)
|
||||
|
||||
@@ -1,22 +1,76 @@
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
private let subsystemIdentifier = Bundle.main.bundleIdentifier ?? "org.amnezia.AmneziaVPN"
|
||||
private let wireGuardSystemLogger = Logger(subsystem: subsystemIdentifier, category: "WireGuard")
|
||||
private let openVPNSystemLogger = Logger(subsystem: subsystemIdentifier, category: "OpenVPN")
|
||||
private let xraySystemLogger = Logger(subsystem: subsystemIdentifier, category: "Xray")
|
||||
private let networkExtensionLogger = Logger(subsystem: subsystemIdentifier, category: "NetworkExtension")
|
||||
|
||||
private func logToSystem(_ logger: Logger, type: OSLogType, prefix: String, title: String, message: String) {
|
||||
let combinedTitle: String
|
||||
if title.isEmpty {
|
||||
combinedTitle = prefix
|
||||
} else {
|
||||
combinedTitle = "\(prefix): \(title)"
|
||||
}
|
||||
|
||||
switch type {
|
||||
case .debug:
|
||||
if combinedTitle.isEmpty {
|
||||
logger.debug("\(message, privacy: .public)")
|
||||
} else {
|
||||
logger.debug("\(combinedTitle, privacy: .public) \(message, privacy: .public)")
|
||||
}
|
||||
case .info:
|
||||
if combinedTitle.isEmpty {
|
||||
logger.info("\(message, privacy: .public)")
|
||||
} else {
|
||||
logger.info("\(combinedTitle, privacy: .public) \(message, privacy: .public)")
|
||||
}
|
||||
case .error:
|
||||
if combinedTitle.isEmpty {
|
||||
logger.error("\(message, privacy: .public)")
|
||||
} else {
|
||||
logger.error("\(combinedTitle, privacy: .public) \(message, privacy: .public)")
|
||||
}
|
||||
case .fault:
|
||||
if combinedTitle.isEmpty {
|
||||
logger.fault("\(message, privacy: .public)")
|
||||
} else {
|
||||
logger.fault("\(combinedTitle, privacy: .public) \(message, privacy: .public)")
|
||||
}
|
||||
default:
|
||||
if combinedTitle.isEmpty {
|
||||
logger.log("\(message, privacy: .public)")
|
||||
} else {
|
||||
logger.log("\(combinedTitle, privacy: .public) \(message, privacy: .public)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func wg_log(_ type: OSLogType, title: String = "", staticMessage: StaticString) {
|
||||
neLog(type, title: "WG: \(title)", message: "\(staticMessage)")
|
||||
let stringMessage = String(describing: staticMessage)
|
||||
logToSystem(wireGuardSystemLogger, type: type, prefix: "WG", title: title, message: stringMessage)
|
||||
neLog(type, title: "WG: \(title)", message: stringMessage)
|
||||
}
|
||||
|
||||
public func wg_log(_ type: OSLogType, title: String = "", message: String) {
|
||||
logToSystem(wireGuardSystemLogger, type: type, prefix: "WG", title: title, message: message)
|
||||
neLog(type, title: "WG: \(title)", message: message)
|
||||
}
|
||||
|
||||
public func ovpnLog(_ type: OSLogType, title: String = "", message: String) {
|
||||
logToSystem(openVPNSystemLogger, type: type, prefix: "OVPN", title: title, message: message)
|
||||
neLog(type, title: "OVPN: \(title)", message: message)
|
||||
}
|
||||
|
||||
public func xrayLog(_ type: OSLogType, title: String = "", message: String) {
|
||||
logToSystem(xraySystemLogger, type: type, prefix: "XRAY", title: title, message: message)
|
||||
neLog(type, title: "XRAY: \(title)", message: message)
|
||||
}
|
||||
|
||||
public func neLog(_ type: OSLogType, title: String = "", message: String) {
|
||||
logToSystem(networkExtensionLogger, type: type, prefix: "NE", title: title, message: message)
|
||||
Log.log(type, title: "NE: \(title)", message: message)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import Foundation
|
||||
import NetworkExtension
|
||||
import OpenVPNAdapter
|
||||
import CryptoKit
|
||||
|
||||
struct OpenVPNConfig: Decodable {
|
||||
let config: String
|
||||
@@ -27,26 +28,83 @@ extension PacketTunnelProvider {
|
||||
let ovpnConfiguration = Data(openVPNConfig.config.utf8)
|
||||
setupAndlaunchOpenVPN(withConfig: ovpnConfiguration, completionHandler: completionHandler)
|
||||
} catch {
|
||||
ovpnLog(.error, message: "Can't parse config: \(error.localizedDescription)")
|
||||
|
||||
if let underlyingError = (error as NSError).userInfo[NSUnderlyingErrorKey] as? NSError {
|
||||
ovpnLog(.error, message: "Can't parse config: \(underlyingError.localizedDescription)")
|
||||
}
|
||||
|
||||
ovpnLog(.error, message: "Can't parse OpenVPN config: \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private func logOpenVPNError(_ error: NSError) {
|
||||
let fatalFlag = (error.userInfo[OpenVPNAdapterErrorFatalKey] as? Bool) ?? false
|
||||
var lines: [String] = []
|
||||
lines.append("domain=\(error.domain) code=\(error.code) fatal=\(fatalFlag)")
|
||||
|
||||
if let adapterMessage = error.userInfo[OpenVPNAdapterErrorMessageKey] as? String, !adapterMessage.isEmpty {
|
||||
lines.append("message=\(adapterMessage)")
|
||||
}
|
||||
|
||||
let userInfoKeys = error.userInfo.keys.map { String(describing: $0) }.sorted()
|
||||
if !userInfoKeys.isEmpty {
|
||||
lines.append("userInfoKeys=[\(userInfoKeys.joined(separator: ","))]")
|
||||
}
|
||||
|
||||
if let underlying = error.userInfo[NSUnderlyingErrorKey] as? NSError {
|
||||
lines.append("underlying=\(underlying.domain)#\(underlying.code) fatal=\((underlying.userInfo[OpenVPNAdapterErrorFatalKey] as? Bool) ?? false)")
|
||||
if let underlyingMessage = underlying.userInfo[OpenVPNAdapterErrorMessageKey] as? String, !underlyingMessage.isEmpty {
|
||||
lines.append("underlyingMessage=\(underlyingMessage)")
|
||||
} else if !underlying.localizedDescription.isEmpty {
|
||||
lines.append("underlyingLocalized=\(underlying.localizedDescription)")
|
||||
}
|
||||
} else if let underlying = error.userInfo[NSUnderlyingErrorKey] {
|
||||
lines.append("underlyingRaw=\(underlying)")
|
||||
}
|
||||
|
||||
let formatted = lines.joined(separator: "\n ")
|
||||
ovpnLog(.error, title: "Error", message: formatted)
|
||||
}
|
||||
|
||||
private func setupAndlaunchOpenVPN(withConfig ovpnConfiguration: Data,
|
||||
withShadowSocks viaSS: Bool = false,
|
||||
completionHandler: @escaping (Error?) -> Void) {
|
||||
ovpnLog(.info, message: "Setup and launch")
|
||||
|
||||
let str = String(decoding: ovpnConfiguration, as: UTF8.self)
|
||||
var configString = String(decoding: ovpnConfiguration, as: UTF8.self)
|
||||
|
||||
let digest = SHA256.hash(data: ovpnConfiguration)
|
||||
let digestString = digest.map { String(format: "%02x", $0) }.joined()
|
||||
ovpnLog(.info, title: "ConfigDigest", message: digestString)
|
||||
|
||||
let hasTlsAuthOpen = configString.contains("<tls-auth>")
|
||||
let hasTlsAuthClose = configString.contains("</tls-auth>")
|
||||
ovpnLog(.info, title: "ConfigFlags", message: "tls-auth open=\(hasTlsAuthOpen) close=\(hasTlsAuthClose)")
|
||||
|
||||
let lines = configString.split(separator: "\n")
|
||||
let head = lines.prefix(10).joined(separator: "\n")
|
||||
let tail = lines.suffix(10).joined(separator: "\n")
|
||||
ovpnLog(.debug, title: "ConfigHead", message: head)
|
||||
ovpnLog(.debug, title: "ConfigTail", message: tail)
|
||||
|
||||
if let start = configString.range(of: "<tls-auth>"),
|
||||
let end = configString.range(of: "</tls-auth>", range: start.upperBound..<configString.endIndex) {
|
||||
let keyBody = String(configString[start.upperBound..<end.lowerBound])
|
||||
ovpnLog(.debug, title: "TLSAuthInline", message: keyBody)
|
||||
let sanitizedLines = keyBody
|
||||
.split(whereSeparator: { $0.isNewline })
|
||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
.filter { !$0.isEmpty }
|
||||
.filter { !$0.hasPrefix("#") }
|
||||
|
||||
let sanitizedKey = sanitizedLines.joined(separator: "\n")
|
||||
ovpnLog(.debug, title: "TLSAuthSanitized", message: sanitizedKey)
|
||||
let sanitizedBlock = "<tls-auth>\n\(sanitizedKey)\n</tls-auth>"
|
||||
configString.replaceSubrange(start.lowerBound..<end.upperBound, with: sanitizedBlock)
|
||||
}
|
||||
|
||||
let normalizedConfig = configString.replacingOccurrences(of: "\r\n", with: "\n")
|
||||
let sanitizedData = Data(normalizedConfig.utf8)
|
||||
|
||||
let configuration = OpenVPNConfiguration()
|
||||
configuration.fileContent = ovpnConfiguration
|
||||
if str.contains("cloak") {
|
||||
configuration.fileContent = sanitizedData
|
||||
if configString.contains("cloak") {
|
||||
configuration.setPTCloak()
|
||||
}
|
||||
|
||||
@@ -57,6 +115,8 @@ extension PacketTunnelProvider {
|
||||
evaluation = try ovpnAdapter?.apply(configuration: configuration)
|
||||
|
||||
} catch {
|
||||
let nsError = error as NSError
|
||||
ovpnLog(.error, title: "ApplyConfig", message: "domain=\(nsError.domain) code=\(nsError.code) info=\(nsError.userInfo)")
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
@@ -71,7 +131,7 @@ extension PacketTunnelProvider {
|
||||
}
|
||||
|
||||
startHandler = completionHandler
|
||||
ovpnAdapter?.connect(using: packetFlow)
|
||||
ovpnAdapter?.connect(using: openVPNPacketFlow())
|
||||
}
|
||||
|
||||
func handleOpenVPNStatusMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
|
||||
@@ -93,7 +153,7 @@ extension PacketTunnelProvider {
|
||||
}
|
||||
|
||||
func stopOpenVPN(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
|
||||
ovpnLog(.info, message: "Stopping tunnel: reason: \(reason.description)")
|
||||
ovpnLog(.info, message: "Stopping tunnel: reason: \(reason.amneziaDescription)")
|
||||
|
||||
stopHandler = completionHandler
|
||||
if vpnReachability.isTracking {
|
||||
@@ -208,8 +268,11 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
||||
|
||||
// Handle errors thrown by the OpenVPN library
|
||||
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleError error: Error) {
|
||||
let nsError = error as NSError
|
||||
logOpenVPNError(nsError)
|
||||
|
||||
// Handle only fatal errors
|
||||
guard let fatal = (error as NSError).userInfo[OpenVPNAdapterErrorFatalKey] as? Bool,
|
||||
guard let fatal = nsError.userInfo[OpenVPNAdapterErrorFatalKey] as? Bool,
|
||||
fatal == true else { return }
|
||||
|
||||
if vpnReachability.isTracking {
|
||||
@@ -230,5 +293,3 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
||||
ovpnLog(.info, message: logMessage)
|
||||
}
|
||||
}
|
||||
|
||||
extension NEPacketTunnelFlow: OpenVPNAdapterPacketFlow {}
|
||||
@@ -94,15 +94,24 @@ extension PacketTunnelProvider {
|
||||
}
|
||||
} catch {
|
||||
wg_log(.error, message: "Can't parse WG config: \(error.localizedDescription)")
|
||||
completionHandler(nil)
|
||||
errorNotifier.notify(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid)
|
||||
completionHandler(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func handleWireguardStatusMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
|
||||
guard let completionHandler = completionHandler else { return }
|
||||
wgAdapter?.getRuntimeConfiguration { settings in
|
||||
let components = settings!.components(separatedBy: "\n")
|
||||
guard let wgAdapter = wgAdapter else {
|
||||
completionHandler(nil)
|
||||
return
|
||||
}
|
||||
wgAdapter.getRuntimeConfiguration { settings in
|
||||
guard let settings = settings else {
|
||||
completionHandler(nil)
|
||||
return
|
||||
}
|
||||
let components = settings.components(separatedBy: "\n")
|
||||
|
||||
var settingsDictionary: [String: String] = [:]
|
||||
for component in components {
|
||||
@@ -131,7 +140,7 @@ extension PacketTunnelProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private func handleWireguardAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
|
||||
func handleWireguardAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
|
||||
guard let completionHandler = completionHandler else { return }
|
||||
if messageData.count == 1 && messageData[0] == 0 {
|
||||
wgAdapter?.getRuntimeConfiguration { settings in
|
||||
@@ -176,7 +185,7 @@ extension PacketTunnelProvider {
|
||||
}
|
||||
|
||||
func stopWireguard(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
|
||||
wg_log(.info, message: "Stopping tunnel: reason: \(reason.description)")
|
||||
wg_log(.info, message: "Stopping tunnel: reason: \(reason.amneziaDescription)")
|
||||
|
||||
wgAdapter?.stop { error in
|
||||
ErrorNotifier.removeLastErrorFile()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import Foundation
|
||||
import NetworkExtension
|
||||
import WireGuardKitGo
|
||||
|
||||
enum XrayErrors: Error {
|
||||
case noXrayConfig
|
||||
@@ -108,6 +107,8 @@ extension PacketTunnelProvider {
|
||||
return
|
||||
}
|
||||
|
||||
self?.updateActiveInterfaceIndexForCurrentPath()
|
||||
|
||||
// Launch xray
|
||||
self?.setupAndStartXray(configData: updatedData) { xrayError in
|
||||
if let xrayError {
|
||||
@@ -134,6 +135,15 @@ extension PacketTunnelProvider {
|
||||
completionHandler()
|
||||
}
|
||||
|
||||
func sockCallback(fd: uintptr_t) {
|
||||
if activeIfaceIdx != 0 {
|
||||
withUnsafePointer(to: activeIfaceIdx) { ptr in
|
||||
setsockopt(Int32(fd), IPPROTO_IP, IP_BOUND_IF, ptr, socklen_t(MemoryLayout<UInt32>.size))
|
||||
setsockopt(Int32(fd), IPPROTO_IPV6, IPV6_BOUND_IF, ptr, socklen_t(MemoryLayout<UInt32>.size))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setupAndStartXray(configData: Data,
|
||||
completionHandler: @escaping (Error?) -> Void) {
|
||||
let path = Constants.cachesDirectory.appendingPathComponent("config.json", isDirectory: false).path
|
||||
@@ -143,6 +153,17 @@ extension PacketTunnelProvider {
|
||||
return
|
||||
}
|
||||
|
||||
updateActiveInterfaceIndexForCurrentPath()
|
||||
|
||||
let ctx = Unmanaged.passUnretained(self).toOpaque()
|
||||
let cb: libxray_sockcallback = { (fd, ctx) in
|
||||
guard let ctx = ctx else { return }
|
||||
let instance = Unmanaged<PacketTunnelProvider>.fromOpaque(ctx).takeUnretainedValue()
|
||||
|
||||
instance.sockCallback(fd: fd)
|
||||
}
|
||||
LibXraySetSockCallback(cb, ctx)
|
||||
|
||||
LibXrayRunXray(nil,
|
||||
path,
|
||||
Int64.max)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
import NetworkExtension
|
||||
import Network
|
||||
import os
|
||||
import Darwin
|
||||
import OpenVPNAdapter
|
||||
@@ -38,6 +39,12 @@ struct Constants {
|
||||
class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
var wgAdapter: WireGuardAdapter?
|
||||
var ovpnAdapter: OpenVPNAdapter?
|
||||
private lazy var openVPNPacketFlowAdapter = PacketTunnelFlowAdapter(flow: packetFlow)
|
||||
private let pathMonitorQueue = DispatchQueue(label: Constants.processQueueName + ".path-monitor")
|
||||
private let pathMonitor = NWPathMonitor()
|
||||
private var didReceiveInitialPathUpdate = false
|
||||
private var currentPath: Network.NWPath?
|
||||
private var currentPathSignature: String?
|
||||
|
||||
var splitTunnelType: Int?
|
||||
var splitTunnelSites: [String]?
|
||||
@@ -47,8 +54,90 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
var startHandler: ((Error?) -> Void)?
|
||||
var stopHandler: (() -> Void)?
|
||||
var protoType: TunnelProtoType?
|
||||
|
||||
var activeIfaceIdx: UInt32 = 0
|
||||
|
||||
func openVPNPacketFlow() -> OpenVPNAdapterPacketFlow {
|
||||
openVPNPacketFlowAdapter
|
||||
}
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
pathMonitor.pathUpdateHandler = { [weak self] path in
|
||||
guard let self else { return }
|
||||
self.currentPath = path
|
||||
let signature = self.pathSignature(for: path)
|
||||
let hasMeaningfulChange = self.currentPathSignature != signature
|
||||
self.currentPathSignature = signature
|
||||
self.updateActiveInterfaceIndex(for: path)
|
||||
|
||||
guard self.didReceiveInitialPathUpdate else {
|
||||
self.didReceiveInitialPathUpdate = true
|
||||
return
|
||||
}
|
||||
|
||||
guard hasMeaningfulChange, let proto = self.protoType else { return }
|
||||
|
||||
// WireGuard/AWG manages network changes internally; avoid restarting the tunnel here.
|
||||
if proto == .wireguard {
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.handle(networkChange: path) { _ in }
|
||||
}
|
||||
}
|
||||
pathMonitor.start(queue: pathMonitorQueue)
|
||||
|
||||
currentPath = pathMonitor.currentPath
|
||||
currentPathSignature = pathSignature(for: pathMonitor.currentPath)
|
||||
}
|
||||
|
||||
func updateActiveInterfaceIndex(for path: Network.NWPath?) {
|
||||
guard let path else {
|
||||
activeIfaceIdx = 0
|
||||
return
|
||||
}
|
||||
|
||||
let preferredTypes: [NWInterface.InterfaceType] = [.wiredEthernet, .wifi, .cellular, .other]
|
||||
|
||||
let nonLoopbackInterfaces = path.availableInterfaces.filter { $0.type != .loopback }
|
||||
let activeInterfaces = nonLoopbackInterfaces.filter { path.usesInterfaceType($0.type) }
|
||||
|
||||
let candidate = preferredTypes.compactMap { type in
|
||||
activeInterfaces.first { $0.type == type }
|
||||
}.first ?? activeInterfaces.first ?? nonLoopbackInterfaces.first
|
||||
|
||||
if let candidate {
|
||||
activeIfaceIdx = UInt32(candidate.index)
|
||||
} else {
|
||||
activeIfaceIdx = 0
|
||||
}
|
||||
}
|
||||
|
||||
func updateActiveInterfaceIndexForCurrentPath() {
|
||||
if let currentPath {
|
||||
currentPathSignature = pathSignature(for: currentPath)
|
||||
updateActiveInterfaceIndex(for: currentPath)
|
||||
return
|
||||
}
|
||||
|
||||
currentPath = pathMonitor.currentPath
|
||||
currentPathSignature = pathSignature(for: pathMonitor.currentPath)
|
||||
updateActiveInterfaceIndex(for: pathMonitor.currentPath)
|
||||
}
|
||||
|
||||
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
|
||||
if messageData.count == 1 && messageData[0] == 0 {
|
||||
guard let completionHandler else { return }
|
||||
if protoType == .wireguard {
|
||||
handleWireguardAppMessage(messageData, completionHandler: completionHandler)
|
||||
} else {
|
||||
completionHandler(nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
guard let message = String(data: messageData, encoding: .utf8) else {
|
||||
if let completionHandler {
|
||||
completionHandler(nil)
|
||||
@@ -59,6 +148,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
neLog(.info, title: "App said: ", message: message)
|
||||
|
||||
guard let message = try? JSONSerialization.jsonObject(with: messageData, options: []) as? [String: Any] else {
|
||||
if protoType == .wireguard {
|
||||
handleWireguardAppMessage(messageData, completionHandler: completionHandler)
|
||||
return
|
||||
}
|
||||
neLog(.error, message: "Failed to serialize message from app")
|
||||
return
|
||||
}
|
||||
@@ -104,6 +197,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
return
|
||||
}
|
||||
|
||||
didReceiveInitialPathUpdate = false
|
||||
updateActiveInterfaceIndexForCurrentPath()
|
||||
|
||||
switch protoType {
|
||||
case .wireguard:
|
||||
startWireguard(activationAttemptId: activationAttemptId,
|
||||
@@ -157,28 +253,63 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
of object: Any?,
|
||||
change: [NSKeyValueChangeKey: Any]?,
|
||||
context: UnsafeMutableRawPointer?) {
|
||||
guard Constants.kDefaultPathKey != keyPath else { return }
|
||||
// Since iOS 11, we have observed that this KVO event fires repeatedly when connecting over Wifi,
|
||||
// even though the underlying network has not changed (i.e. `isEqualToPath` returns false),
|
||||
// leading to "wakeup crashes" due to excessive network activity. Guard against false positives by
|
||||
// comparing the paths' string description, which includes properties not exposed by the class
|
||||
guard let lastPath: NWPath = change?[.oldKey] as? NWPath,
|
||||
let defPath = defaultPath,
|
||||
lastPath != defPath || lastPath.description != defPath.description else {
|
||||
guard Constants.kDefaultPathKey == keyPath else {
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self, self.defaultPath != nil else { return }
|
||||
self.handle(networkChange: self.defaultPath!) { _ in }
|
||||
}
|
||||
}
|
||||
|
||||
private func handle(networkChange changePath: NWPath, completion: @escaping (Error?) -> Void) {
|
||||
private func handle(networkChange changePath: Network.NWPath, completion: @escaping (Error?) -> Void) {
|
||||
updateActiveInterfaceIndex(for: changePath)
|
||||
wg_log(.info, message: "Tunnel restarted.")
|
||||
startTunnel(options: nil, completionHandler: completion)
|
||||
}
|
||||
}
|
||||
|
||||
private extension PacketTunnelProvider {
|
||||
func pathSignature(for path: Network.NWPath) -> String {
|
||||
var signatureComponents = [String(describing: path.status)]
|
||||
signatureComponents.append(path.isExpensive ? "exp" : "noexp")
|
||||
signatureComponents.append(path.isConstrained ? "con" : "nocon")
|
||||
|
||||
let preferredTypes: [NWInterface.InterfaceType] = [.wiredEthernet, .wifi, .cellular, .loopback, .other]
|
||||
let sortedInterfaces = path.availableInterfaces.sorted { lhs, rhs in
|
||||
if lhs.type == rhs.type {
|
||||
return lhs.index < rhs.index
|
||||
}
|
||||
|
||||
let lhsOrder = preferredTypes.firstIndex(of: lhs.type) ?? preferredTypes.count
|
||||
let rhsOrder = preferredTypes.firstIndex(of: rhs.type) ?? preferredTypes.count
|
||||
|
||||
if lhsOrder == rhsOrder {
|
||||
return lhs.index < rhs.index
|
||||
}
|
||||
|
||||
return lhsOrder < rhsOrder
|
||||
}
|
||||
|
||||
for interface in sortedInterfaces {
|
||||
let typeName: String
|
||||
switch interface.type {
|
||||
case .wiredEthernet: typeName = "ethernet"
|
||||
case .wifi: typeName = "wifi"
|
||||
case .cellular: typeName = "cellular"
|
||||
case .loopback: typeName = "loopback"
|
||||
case .other: typeName = "other"
|
||||
@unknown default: typeName = "unknown"
|
||||
}
|
||||
signatureComponents.append("\(typeName):\(interface.index)")
|
||||
}
|
||||
|
||||
// Include currently used interface preference ordering
|
||||
for type in preferredTypes {
|
||||
let usesType = path.usesInterfaceType(type)
|
||||
signatureComponents.append("uses-\(type):\(usesType)")
|
||||
}
|
||||
|
||||
return signatureComponents.joined(separator: "|")
|
||||
}
|
||||
}
|
||||
|
||||
extension WireGuardLogLevel {
|
||||
var osLogLevel: OSLogType {
|
||||
switch self {
|
||||
@@ -190,8 +321,27 @@ extension WireGuardLogLevel {
|
||||
}
|
||||
}
|
||||
|
||||
extension NEProviderStopReason: CustomStringConvertible {
|
||||
public var description: String {
|
||||
final class PacketTunnelFlowAdapter: NSObject, OpenVPNAdapterPacketFlow {
|
||||
private let flow: NEPacketTunnelFlow
|
||||
|
||||
init(flow: NEPacketTunnelFlow) {
|
||||
self.flow = flow
|
||||
super.init()
|
||||
}
|
||||
|
||||
@objc(readPacketsWithCompletionHandler:)
|
||||
func readPackets(completionHandler: @escaping ([Data], [NSNumber]) -> Void) {
|
||||
flow.readPackets(completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@objc(writePackets:withProtocols:)
|
||||
func writePackets(_ packets: [Data], withProtocols protocols: [NSNumber]) -> Bool {
|
||||
flow.writePackets(packets, withProtocols: protocols)
|
||||
}
|
||||
}
|
||||
|
||||
extension NEProviderStopReason {
|
||||
var amneziaDescription: String {
|
||||
switch self {
|
||||
case .none:
|
||||
return "No specific reason"
|
||||
@@ -223,6 +373,8 @@ extension NEProviderStopReason: CustomStringConvertible {
|
||||
return "The current console user changed"
|
||||
case .connectionFailed:
|
||||
return "The connection failed"
|
||||
case .internalError:
|
||||
return "The network extension reported an internal error"
|
||||
case .sleep:
|
||||
return "A stop reason indicating the VPNC enabled disconnect on sleep and the device went to sleep"
|
||||
case .appUpdate:
|
||||
|
||||
@@ -11,13 +11,7 @@ class ScreenProtection {
|
||||
import UIKit
|
||||
|
||||
public func toggleScreenshots(_ isEnabled: Bool) {
|
||||
let window = UIApplication.shared.keyWindows.first!
|
||||
|
||||
if isEnabled {
|
||||
ScreenProtection.shared.disable(for: window.rootViewController!.view)
|
||||
} else {
|
||||
ScreenProtection.shared.enable(for: window.rootViewController!.view)
|
||||
}
|
||||
ScreenProtection.shared.setScreenshotsEnabled(isEnabled)
|
||||
}
|
||||
|
||||
extension UIApplication {
|
||||
@@ -45,6 +39,45 @@ class ScreenProtection {
|
||||
|
||||
private var blurView: UIVisualEffectView?
|
||||
private var recordingObservation: NSKeyValueObservation?
|
||||
private var desiredScreenshotsEnabled: Bool?
|
||||
private var retryCount = 0
|
||||
private var retryWorkItem: DispatchWorkItem?
|
||||
|
||||
public func setScreenshotsEnabled(_ isEnabled: Bool) {
|
||||
DispatchQueue.main.async {
|
||||
self.desiredScreenshotsEnabled = isEnabled
|
||||
self.applyScreenshotsSettingOrRetry()
|
||||
}
|
||||
}
|
||||
|
||||
private func applyScreenshotsSettingOrRetry() {
|
||||
assert(Thread.isMainThread)
|
||||
|
||||
guard let desiredScreenshotsEnabled else { return }
|
||||
guard let window = UIApplication.shared.keyWindows.first,
|
||||
let rootView = window.rootViewController?.view else {
|
||||
retryCount += 1
|
||||
guard retryCount <= 50 else { return } // ~5s total
|
||||
|
||||
retryWorkItem?.cancel()
|
||||
let item = DispatchWorkItem { [weak self] in
|
||||
self?.applyScreenshotsSettingOrRetry()
|
||||
}
|
||||
retryWorkItem = item
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: item)
|
||||
return
|
||||
}
|
||||
|
||||
retryWorkItem?.cancel()
|
||||
retryWorkItem = nil
|
||||
retryCount = 0
|
||||
|
||||
if desiredScreenshotsEnabled {
|
||||
disable(for: rootView)
|
||||
} else {
|
||||
enable(for: rootView)
|
||||
}
|
||||
}
|
||||
|
||||
public func enable(for view: UIView) {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#ifndef STOREKITCONTROLLER_H
|
||||
#define STOREKITCONTROLLER_H
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <StoreKit/StoreKit.h>
|
||||
|
||||
@class Product;
|
||||
@class Transaction;
|
||||
@class VerificationResult;
|
||||
|
||||
API_AVAILABLE(ios(15.0), macos(12.0))
|
||||
@interface StoreKitController : NSObject
|
||||
|
||||
+ (instancetype)sharedInstance;
|
||||
|
||||
- (void)purchaseProduct:(NSString *)productIdentifier
|
||||
completion:(void (^)(BOOL success,
|
||||
NSString *_Nullable transactionId,
|
||||
NSString *_Nullable productId,
|
||||
NSString *_Nullable originalTransactionId,
|
||||
NSError *_Nullable error))completion;
|
||||
|
||||
- (void)restorePurchasesWithCompletion:(void (^)(BOOL success,
|
||||
NSArray<NSDictionary *> *_Nullable restoredTransactions,
|
||||
NSError *_Nullable error))completion;
|
||||
|
||||
// Fetch product information for a set of identifiers without initiating a purchase
|
||||
- (void)fetchProductsWithIdentifiers:(NSSet<NSString *> *)productIdentifiers
|
||||
completion:(void (^)(NSArray<NSDictionary *> *products,
|
||||
NSArray<NSString *> *invalidIdentifiers,
|
||||
NSError *_Nullable error))completion;
|
||||
|
||||
@end
|
||||
|
||||
#endif // STOREKITCONTROLLER_H
|
||||
@@ -0,0 +1,264 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#import "StoreKitController.h"
|
||||
#import <StoreKit/StoreKit.h>
|
||||
|
||||
#include <QtCore/QDebug>
|
||||
#include <QtCore/QString>
|
||||
|
||||
API_AVAILABLE(ios(15.0), macos(12.0))
|
||||
@interface StoreKitController () <SKProductsRequestDelegate, SKPaymentTransactionObserver>
|
||||
@property (nonatomic, copy) void (^purchaseCompletion)(BOOL success,
|
||||
NSString *_Nullable transactionId,
|
||||
NSString *_Nullable productId,
|
||||
NSString *_Nullable originalTransactionId,
|
||||
NSError *_Nullable error);
|
||||
@property (nonatomic, copy) void (^restoreCompletion)(BOOL success,
|
||||
NSArray<NSDictionary *> *_Nullable restoredTransactions,
|
||||
NSError *_Nullable error);
|
||||
@property (nonatomic, copy) void (^productsFetchCompletion)(NSArray<NSDictionary *> *products,
|
||||
NSArray<NSString *> *invalidIdentifiers,
|
||||
NSError *_Nullable error);
|
||||
@property (nonatomic, strong) SKProductsRequest *productsRequest;
|
||||
@property (nonatomic, strong) NSMutableArray<NSDictionary *> *restoredTransactions;
|
||||
@end
|
||||
|
||||
@implementation StoreKitController
|
||||
|
||||
+ (instancetype)sharedInstance
|
||||
{
|
||||
static dispatch_once_t onceToken;
|
||||
static StoreKitController *instance;
|
||||
dispatch_once(&onceToken, ^{
|
||||
if (@available(iOS 15.0, macOS 12.0, *)) {
|
||||
instance = [[StoreKitController alloc] init];
|
||||
}
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
|
||||
- (instancetype)init API_AVAILABLE(ios(15.0), macos(12.0))
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
|
||||
}
|
||||
|
||||
- (void)purchaseProduct:(NSString *)productIdentifier
|
||||
completion:(void (^)(BOOL success,
|
||||
NSString *_Nullable transactionId,
|
||||
NSString *_Nullable productId,
|
||||
NSString *_Nullable originalTransactionId,
|
||||
NSError *_Nullable error))completion API_AVAILABLE(ios(15.0), macos(12.0))
|
||||
{
|
||||
self.purchaseCompletion = completion;
|
||||
|
||||
qInfo().noquote() << "[IAP][StoreKit] Starting purchase for" << QString::fromUtf8(productIdentifier.UTF8String);
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
[self performPurchaseAsync:productIdentifier];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)performPurchaseAsync:(NSString *)productIdentifier API_AVAILABLE(ios(15.0), macos(12.0))
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
@try {
|
||||
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithObject:productIdentifier]];
|
||||
request.delegate = self;
|
||||
[request start];
|
||||
|
||||
} @catch (NSException *exception) {
|
||||
NSError *error = [NSError errorWithDomain:@"StoreKitController"
|
||||
code:1
|
||||
userInfo:@{ NSLocalizedDescriptionKey : exception.reason ?: @"Purchase failed" }];
|
||||
if (self.purchaseCompletion) {
|
||||
self.purchaseCompletion(NO, nil, nil, nil, error);
|
||||
self.purchaseCompletion = nil;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)restorePurchasesWithCompletion:(void (^)(BOOL success,
|
||||
NSArray<NSDictionary *> *_Nullable restoredTransactions,
|
||||
NSError *_Nullable error))completion API_AVAILABLE(ios(15.0), macos(12.0))
|
||||
{
|
||||
self.restoreCompletion = completion;
|
||||
self.restoredTransactions = [NSMutableArray array];
|
||||
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
|
||||
}
|
||||
|
||||
- (void)fetchProductsWithIdentifiers:(NSSet<NSString *> *)productIdentifiers
|
||||
completion:(void (^)(NSArray<NSDictionary *> *products,
|
||||
NSArray<NSString *> *invalidIdentifiers,
|
||||
NSError *_Nullable error))completion API_AVAILABLE(ios(15.0), macos(12.0))
|
||||
{
|
||||
self.productsFetchCompletion = completion;
|
||||
self.productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers];
|
||||
self.productsRequest.delegate = self;
|
||||
[self.productsRequest start];
|
||||
}
|
||||
|
||||
#pragma mark - SKProductsRequestDelegate / SKRequestDelegate
|
||||
|
||||
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
|
||||
{
|
||||
if (self.purchaseCompletion) {
|
||||
SKProduct *product = response.products.firstObject;
|
||||
if (!product) {
|
||||
NSError *error = [NSError errorWithDomain:@"StoreKitController"
|
||||
code:0
|
||||
userInfo:@{ NSLocalizedDescriptionKey : @"Product not found" }];
|
||||
self.purchaseCompletion(NO, nil, nil, nil, error);
|
||||
self.purchaseCompletion = nil;
|
||||
self.productsRequest = nil;
|
||||
return;
|
||||
}
|
||||
NSString *currencyCode = [product.priceLocale objectForKey:NSLocaleCurrencyCode] ?: @"";
|
||||
NSString *priceString = [product.price stringValue] ?: @"";
|
||||
qInfo().noquote() << "[IAP][StoreKit] Received product" << QString::fromUtf8(product.productIdentifier.UTF8String)
|
||||
<< "price=" << QString::fromUtf8(priceString.UTF8String)
|
||||
<< "currency=" << QString::fromUtf8(currencyCode.UTF8String);
|
||||
SKPayment *payment = [SKPayment paymentWithProduct:product];
|
||||
[[SKPaymentQueue defaultQueue] addPayment:payment];
|
||||
self.productsRequest = nil;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.productsFetchCompletion) {
|
||||
NSMutableArray<NSDictionary *> *productDicts = [NSMutableArray array];
|
||||
for (SKProduct *p in response.products) {
|
||||
NSDictionary *productDict = @{
|
||||
@"productId": p.productIdentifier,
|
||||
@"title": p.localizedTitle,
|
||||
@"description": p.localizedDescription,
|
||||
@"price": p.price.stringValue,
|
||||
@"currencyCode": [p.priceLocale objectForKey:NSLocaleCurrencyCode] ?: @""
|
||||
};
|
||||
[productDicts addObject:productDict];
|
||||
NSString *productCurrency = [p.priceLocale objectForKey:NSLocaleCurrencyCode] ?: @"";
|
||||
NSString *productPrice = [p.price stringValue] ?: @"";
|
||||
qInfo().noquote() << "[IAP][StoreKit] Fetched product info" << QString::fromUtf8(p.productIdentifier.UTF8String)
|
||||
<< "price=" << QString::fromUtf8(productPrice.UTF8String)
|
||||
<< "currency=" << QString::fromUtf8(productCurrency.UTF8String);
|
||||
}
|
||||
|
||||
self.productsFetchCompletion(productDicts, response.invalidProductIdentifiers, nil);
|
||||
self.productsFetchCompletion = nil;
|
||||
self.productsRequest = nil;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error
|
||||
{
|
||||
if (self.purchaseCompletion) {
|
||||
self.purchaseCompletion(NO, nil, nil, nil, error);
|
||||
self.purchaseCompletion = nil;
|
||||
}
|
||||
if (self.productsFetchCompletion) {
|
||||
self.productsFetchCompletion(@[], @[], error);
|
||||
self.productsFetchCompletion = nil;
|
||||
}
|
||||
self.productsRequest = nil;
|
||||
}
|
||||
|
||||
#pragma mark - SKPaymentTransactionObserver
|
||||
|
||||
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions
|
||||
{
|
||||
for (SKPaymentTransaction *transaction in transactions) {
|
||||
switch (transaction.transactionState) {
|
||||
case SKPaymentTransactionStatePurchased: {
|
||||
NSString *originalTransactionId = transaction.originalTransaction.transactionIdentifier ?: transaction.transactionIdentifier;
|
||||
qInfo().noquote() << "[IAP][StoreKit] Transaction purchased" << QString::fromUtf8(transaction.transactionIdentifier.UTF8String)
|
||||
<< "original=" << QString::fromUtf8((originalTransactionId ?: @"").UTF8String)
|
||||
<< "product=" << QString::fromUtf8(transaction.payment.productIdentifier.UTF8String);
|
||||
|
||||
if (self.purchaseCompletion) {
|
||||
self.purchaseCompletion(YES,
|
||||
transaction.transactionIdentifier,
|
||||
transaction.payment.productIdentifier,
|
||||
originalTransactionId,
|
||||
nil);
|
||||
self.purchaseCompletion = nil;
|
||||
}
|
||||
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
|
||||
break;
|
||||
}
|
||||
case SKPaymentTransactionStateFailed:
|
||||
qInfo().noquote() << "[IAP][StoreKit] Transaction failed" << QString::fromUtf8(transaction.transactionIdentifier.UTF8String)
|
||||
<< "product=" << QString::fromUtf8(transaction.payment.productIdentifier.UTF8String)
|
||||
<< "error=" << QString::fromUtf8(transaction.error.localizedDescription.UTF8String);
|
||||
if (self.purchaseCompletion) {
|
||||
self.purchaseCompletion(NO,
|
||||
transaction.transactionIdentifier,
|
||||
transaction.payment.productIdentifier,
|
||||
nil,
|
||||
transaction.error);
|
||||
self.purchaseCompletion = nil;
|
||||
}
|
||||
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
|
||||
break;
|
||||
case SKPaymentTransactionStateRestored: {
|
||||
if (self.restoreCompletion) {
|
||||
NSString *transactionId = transaction.transactionIdentifier ?: @"";
|
||||
NSString *originalTransactionId = transaction.originalTransaction.transactionIdentifier ?: transactionId;
|
||||
NSString *productId = transaction.payment.productIdentifier ?: @"";
|
||||
|
||||
qInfo().noquote() << "[IAP][StoreKit] Transaction restored"
|
||||
<< QString::fromUtf8(transactionId.UTF8String)
|
||||
<< "original="
|
||||
<< QString::fromUtf8((originalTransactionId ?: @"").UTF8String)
|
||||
<< "product="
|
||||
<< QString::fromUtf8((productId ?: @"").UTF8String);
|
||||
|
||||
NSDictionary *info = @{
|
||||
@"transactionId": transactionId,
|
||||
@"originalTransactionId": originalTransactionId ?: @"",
|
||||
@"productId": productId ?: @""
|
||||
};
|
||||
if (!self.restoredTransactions) {
|
||||
self.restoredTransactions = [NSMutableArray array];
|
||||
}
|
||||
[self.restoredTransactions addObject:info];
|
||||
}
|
||||
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
|
||||
break;
|
||||
}
|
||||
case SKPaymentTransactionStatePurchasing:
|
||||
case SKPaymentTransactionStateDeferred:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
|
||||
{
|
||||
if (self.restoreCompletion) {
|
||||
NSArray<NSDictionary *> *transactions = [self.restoredTransactions copy];
|
||||
self.restoreCompletion(YES, transactions, nil);
|
||||
self.restoreCompletion = nil;
|
||||
self.restoredTransactions = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error
|
||||
{
|
||||
if (self.restoreCompletion) {
|
||||
self.restoreCompletion(NO, nil, error);
|
||||
self.restoreCompletion = nil;
|
||||
self.restoredTransactions = nil;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -6,8 +6,6 @@ struct WGConfig: Decodable {
|
||||
let junkPacketCount, junkPacketMinSize, junkPacketMaxSize: String?
|
||||
let initPacketJunkSize, responsePacketJunkSize, cookieReplyPacketJunkSize, transportPacketJunkSize: String?
|
||||
let specialJunk1, specialJunk2, specialJunk3, specialJunk4, specialJunk5: String?
|
||||
let controlledJunk1, controlledJunk2, controlledJunk3: String?
|
||||
let specialHandshakeTimeout: String?
|
||||
let dns1: String
|
||||
let dns2: String
|
||||
let mtu: String
|
||||
@@ -28,8 +26,6 @@ struct WGConfig: Decodable {
|
||||
case junkPacketCount = "Jc", junkPacketMinSize = "Jmin", junkPacketMaxSize = "Jmax"
|
||||
case initPacketJunkSize = "S1", responsePacketJunkSize = "S2", cookieReplyPacketJunkSize = "S3", transportPacketJunkSize = "S4"
|
||||
case specialJunk1 = "I1", specialJunk2 = "I2", specialJunk3 = "I3", specialJunk4 = "I4", specialJunk5 = "I5"
|
||||
case controlledJunk1 = "J1", controlledJunk2 = "J2", controlledJunk3 = "J3"
|
||||
case specialHandshakeTimeout = "Itime"
|
||||
case dns1
|
||||
case dns2
|
||||
case mtu
|
||||
@@ -46,58 +42,64 @@ struct WGConfig: Decodable {
|
||||
}
|
||||
|
||||
var settings: String {
|
||||
guard junkPacketCount != nil else { return "" }
|
||||
|
||||
func trimmed(_ value: String?) -> String? {
|
||||
guard let value = value?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!value.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
guard
|
||||
let junkPacketCount = trimmed(junkPacketCount),
|
||||
let junkPacketMinSize = trimmed(junkPacketMinSize),
|
||||
let junkPacketMaxSize = trimmed(junkPacketMaxSize),
|
||||
let initPacketJunkSize = trimmed(initPacketJunkSize),
|
||||
let responsePacketJunkSize = trimmed(responsePacketJunkSize),
|
||||
let initPacketMagicHeader = trimmed(initPacketMagicHeader),
|
||||
let responsePacketMagicHeader = trimmed(responsePacketMagicHeader),
|
||||
let underloadPacketMagicHeader = trimmed(underloadPacketMagicHeader),
|
||||
let transportPacketMagicHeader = trimmed(transportPacketMagicHeader)
|
||||
else { return "" }
|
||||
|
||||
var settingsLines: [String] = []
|
||||
|
||||
|
||||
// Required parameters when junkPacketCount is present
|
||||
settingsLines.append("Jc = \(junkPacketCount!)")
|
||||
settingsLines.append("Jmin = \(junkPacketMinSize!)")
|
||||
settingsLines.append("Jmax = \(junkPacketMaxSize!)")
|
||||
settingsLines.append("S1 = \(initPacketJunkSize!)")
|
||||
settingsLines.append("S2 = \(responsePacketJunkSize!)")
|
||||
|
||||
settingsLines.append("H1 = \(initPacketMagicHeader!)")
|
||||
settingsLines.append("H2 = \(responsePacketMagicHeader!)")
|
||||
settingsLines.append("H3 = \(underloadPacketMagicHeader!)")
|
||||
settingsLines.append("H4 = \(transportPacketMagicHeader!)")
|
||||
settingsLines.append("Jc = \(junkPacketCount)")
|
||||
settingsLines.append("Jmin = \(junkPacketMinSize)")
|
||||
settingsLines.append("Jmax = \(junkPacketMaxSize)")
|
||||
settingsLines.append("S1 = \(initPacketJunkSize)")
|
||||
settingsLines.append("S2 = \(responsePacketJunkSize)")
|
||||
|
||||
settingsLines.append("H1 = \(initPacketMagicHeader)")
|
||||
settingsLines.append("H2 = \(responsePacketMagicHeader)")
|
||||
settingsLines.append("H3 = \(underloadPacketMagicHeader)")
|
||||
settingsLines.append("H4 = \(transportPacketMagicHeader)")
|
||||
|
||||
// Optional parameters - only add if not nil and not empty
|
||||
if let s3 = cookieReplyPacketJunkSize, !s3.isEmpty {
|
||||
if let s3 = trimmed(cookieReplyPacketJunkSize) {
|
||||
settingsLines.append("S3 = \(s3)")
|
||||
}
|
||||
if let s4 = transportPacketJunkSize, !s4.isEmpty {
|
||||
if let s4 = trimmed(transportPacketJunkSize) {
|
||||
settingsLines.append("S4 = \(s4)")
|
||||
}
|
||||
|
||||
if let i1 = specialJunk1, !i1.isEmpty {
|
||||
|
||||
if let i1 = trimmed(specialJunk1) {
|
||||
settingsLines.append("I1 = \(i1)")
|
||||
}
|
||||
if let i2 = specialJunk2, !i2.isEmpty {
|
||||
if let i2 = trimmed(specialJunk2) {
|
||||
settingsLines.append("I2 = \(i2)")
|
||||
}
|
||||
if let i3 = specialJunk3, !i3.isEmpty {
|
||||
if let i3 = trimmed(specialJunk3) {
|
||||
settingsLines.append("I3 = \(i3)")
|
||||
}
|
||||
if let i4 = specialJunk4, !i4.isEmpty {
|
||||
if let i4 = trimmed(specialJunk4) {
|
||||
settingsLines.append("I4 = \(i4)")
|
||||
}
|
||||
if let i5 = specialJunk5, !i5.isEmpty {
|
||||
if let i5 = trimmed(specialJunk5) {
|
||||
settingsLines.append("I5 = \(i5)")
|
||||
}
|
||||
if let j1 = controlledJunk1, !j1.isEmpty {
|
||||
settingsLines.append("J1 = \(j1)")
|
||||
}
|
||||
if let j2 = controlledJunk2, !j2.isEmpty {
|
||||
settingsLines.append("J2 = \(j2)")
|
||||
}
|
||||
if let j3 = controlledJunk3, !j3.isEmpty {
|
||||
settingsLines.append("J3 = \(j3)")
|
||||
}
|
||||
if let itime = specialHandshakeTimeout, !itime.isEmpty {
|
||||
settingsLines.append("Itime = \(itime)")
|
||||
}
|
||||
|
||||
|
||||
return settingsLines.joined(separator: "\n")
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
#define IOS_CONTROLLER_H
|
||||
|
||||
#include "protocols/vpnprotocol.h"
|
||||
#include <functional>
|
||||
#include <QVariant>
|
||||
#include <QVariantMap>
|
||||
#include <QStringList>
|
||||
#include <QList>
|
||||
#include <QElapsedTimer>
|
||||
#include <atomic>
|
||||
|
||||
#ifdef __OBJC__
|
||||
#import <Foundation/Foundation.h>
|
||||
@@ -55,7 +62,24 @@ public:
|
||||
bool shareText(const QStringList &filesToSend);
|
||||
QString openFile();
|
||||
|
||||
void purchaseProduct(const QString &productId,
|
||||
std::function<void(bool success,
|
||||
const QString &transactionId,
|
||||
const QString &purchasedProductId,
|
||||
const QString &originalTransactionId,
|
||||
const QString &errorString)> &&callback);
|
||||
void restorePurchases(std::function<void(bool success,
|
||||
const QList<QVariantMap> &transactions,
|
||||
const QString &errorString)> &&callback);
|
||||
|
||||
// Fetch product info for given product identifiers and return basic fields for logging
|
||||
void fetchProducts(const QStringList &productIds,
|
||||
std::function<void(const QList<QVariantMap> &products,
|
||||
const QStringList &invalidIds,
|
||||
const QString &errorString)> &&callback);
|
||||
|
||||
void requestInetAccess();
|
||||
bool isTestFlight();
|
||||
signals:
|
||||
void connectionStateChanged(Vpn::ConnectionState state);
|
||||
void bytesChanged(quint64 receivedBytes, quint64 sentBytes);
|
||||
@@ -81,6 +105,7 @@ private:
|
||||
bool startXray(const QString &jsonConfig);
|
||||
|
||||
void startTunnel();
|
||||
void emitConnectionStateIfChanged(Vpn::ConnectionState state);
|
||||
|
||||
private:
|
||||
void *m_iosControllerWrapper {};
|
||||
@@ -94,8 +119,13 @@ private:
|
||||
amnezia::Proto m_proto;
|
||||
QJsonObject m_rawConfig;
|
||||
QString m_tunnelId;
|
||||
uint64_t m_txBytes;
|
||||
uint64_t m_rxBytes;
|
||||
uint64_t m_txBytes = 0;
|
||||
uint64_t m_rxBytes = 0;
|
||||
bool m_handshakeAwaiting = false;
|
||||
bool m_handshakeConfirmed = false;
|
||||
QElapsedTimer m_handshakeTimer;
|
||||
Vpn::ConnectionState m_lastEmittedState = Vpn::ConnectionState::Unknown;
|
||||
std::atomic_bool m_statusRequestInFlight { false };
|
||||
};
|
||||
|
||||
#endif // IOS_CONTROLLER_H
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#include "../protocols/vpnprotocol.h"
|
||||
#import "ios_controller_wrapper.h"
|
||||
#import "StoreKitController.h"
|
||||
|
||||
const char* Action::start = "start";
|
||||
const char* Action::restart = "restart";
|
||||
@@ -29,12 +30,46 @@ const char* MessageKey::SplitTunnelSites = "SplitTunnelSites";
|
||||
|
||||
#if !MACOS_NE
|
||||
static UIViewController* getViewController() {
|
||||
NSArray *windows = [[UIApplication sharedApplication]windows];
|
||||
for (UIWindow *window in windows) {
|
||||
if (window.isKeyWindow) {
|
||||
UIApplication *application = [UIApplication sharedApplication];
|
||||
|
||||
if (@available(iOS 13.0, *)) {
|
||||
for (UIScene *scene in application.connectedScenes) {
|
||||
if (scene.activationState != UISceneActivationStateForegroundActive) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (![scene isKindOfClass:[UIWindowScene class]]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
UIWindowScene *windowScene = (UIWindowScene *)scene;
|
||||
|
||||
for (UIWindow *window in windowScene.windows) {
|
||||
if (window.isKeyWindow && window.rootViewController) {
|
||||
return window.rootViewController;
|
||||
}
|
||||
}
|
||||
|
||||
for (UIWindow *window in windowScene.windows) {
|
||||
if (!window.isHidden && window.rootViewController) {
|
||||
return window.rootViewController;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (UIWindow *window in application.windows) {
|
||||
if (window.isKeyWindow && window.rootViewController) {
|
||||
return window.rootViewController;
|
||||
}
|
||||
}
|
||||
|
||||
for (UIWindow *window in application.windows) {
|
||||
if (window.rootViewController) {
|
||||
return window.rootViewController;
|
||||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
#endif
|
||||
@@ -58,6 +93,48 @@ Vpn::ConnectionState iosStatusToState(NEVPNStatus status) {
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
constexpr int kHandshakeTimeoutMs = 12000;
|
||||
constexpr uint64_t kHandshakeRxThreshold = 4096;
|
||||
bool isWireGuardBasedProto(amnezia::Proto proto) {
|
||||
return proto == amnezia::Proto::WireGuard || proto == amnezia::Proto::Awg;
|
||||
}
|
||||
|
||||
uint64_t uint64FromResponse(NSDictionary *response, NSString *key, uint64_t fallback = 0) {
|
||||
id value = response[key];
|
||||
if (!value || value == [NSNull null]) {
|
||||
return fallback;
|
||||
}
|
||||
if ([value isKindOfClass:[NSNumber class]]) {
|
||||
return [(NSNumber *)value unsignedLongLongValue];
|
||||
}
|
||||
if ([value isKindOfClass:[NSString class]]) {
|
||||
const char *str = [(NSString *)value UTF8String];
|
||||
if (str && *str) {
|
||||
return strtoull(str, nullptr, 10);
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
long long int64FromResponse(NSDictionary *response, NSString *key, long long fallback = 0) {
|
||||
id value = response[key];
|
||||
if (!value || value == [NSNull null]) {
|
||||
return fallback;
|
||||
}
|
||||
if ([value isKindOfClass:[NSNumber class]]) {
|
||||
return [(NSNumber *)value longLongValue];
|
||||
}
|
||||
if ([value isKindOfClass:[NSString class]]) {
|
||||
const char *str = [(NSString *)value UTF8String];
|
||||
if (str && *str) {
|
||||
return strtoll(str, nullptr, 10);
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
IosController* s_instance = nullptr;
|
||||
}
|
||||
@@ -67,6 +144,9 @@ IosController::IosController() : QObject()
|
||||
s_instance = this;
|
||||
m_iosControllerWrapper = [[IosControllerWrapper alloc] initWithCppController:this];
|
||||
|
||||
// Initialize StoreKitController early to start observing the payment queue
|
||||
[StoreKitController sharedInstance];
|
||||
|
||||
[[NSNotificationCenter defaultCenter]
|
||||
removeObserver: (__bridge NSObject *)m_iosControllerWrapper];
|
||||
[[NSNotificationCenter defaultCenter]
|
||||
@@ -76,6 +156,15 @@ IosController::IosController() : QObject()
|
||||
|
||||
}
|
||||
|
||||
void IosController::emitConnectionStateIfChanged(Vpn::ConnectionState state)
|
||||
{
|
||||
if (m_lastEmittedState == state) {
|
||||
return;
|
||||
}
|
||||
m_lastEmittedState = state;
|
||||
emit connectionStateChanged(state);
|
||||
}
|
||||
|
||||
IosController* IosController::Instance() {
|
||||
if (!s_instance) {
|
||||
s_instance = new IosController();
|
||||
@@ -242,33 +331,65 @@ void IosController::disconnectVpn()
|
||||
|
||||
void IosController::checkStatus()
|
||||
{
|
||||
if (!m_currentTunnel) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_currentTunnel.connection.status != NEVPNStatusConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_statusRequestInFlight.exchange(true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *actionKey = [NSString stringWithUTF8String:MessageKey::action];
|
||||
NSString *actionValue = [NSString stringWithUTF8String:Action::getStatus];
|
||||
NSString *tunnelIdKey = [NSString stringWithUTF8String:MessageKey::tunnelId];
|
||||
NSString *tunnelIdValue = !m_tunnelId.isEmpty() ? m_tunnelId.toNSString() : @"";
|
||||
|
||||
NSDictionary* message = @{actionKey: actionValue, tunnelIdKey: tunnelIdValue};
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
sendVpnExtensionMessage(message, [&](NSDictionary* response){
|
||||
uint64_t txBytes = [response[@"tx_bytes"] intValue];
|
||||
uint64_t rxBytes = [response[@"rx_bytes"] intValue];
|
||||
|
||||
uint64_t last_handshake_time_sec = 0;
|
||||
#if !MACOS_NE
|
||||
if (response[@"last_handshake_time_sec"] && ![response[@"last_handshake_time_sec"] isKindOfClass:[NSNull class]]) {
|
||||
last_handshake_time_sec = [response[@"last_handshake_time_sec"] intValue];
|
||||
} else {
|
||||
qDebug() << "Key last_handshake_time_sec is missing or null";
|
||||
if (!response) {
|
||||
QMetaObject::invokeMethod(this, [this]() {
|
||||
m_statusRequestInFlight = false;
|
||||
}, Qt::QueuedConnection);
|
||||
return;
|
||||
}
|
||||
|
||||
if (last_handshake_time_sec < 0) {
|
||||
disconnectVpn();
|
||||
qDebug() << "Invalid handshake time, disconnecting VPN.";
|
||||
}
|
||||
#endif
|
||||
const uint64_t txBytes = uint64FromResponse(response, @"tx_bytes");
|
||||
const uint64_t rxBytes = uint64FromResponse(response, @"rx_bytes");
|
||||
const long long last_handshake_time_sec = int64FromResponse(response, @"last_handshake_time_sec");
|
||||
|
||||
emit bytesChanged(rxBytes - m_rxBytes, txBytes - m_txBytes);
|
||||
m_rxBytes = rxBytes;
|
||||
m_txBytes = txBytes;
|
||||
QMetaObject::invokeMethod(this, [this, txBytes, rxBytes, last_handshake_time_sec]() {
|
||||
if (isWireGuardBasedProto(m_proto) && m_handshakeAwaiting) {
|
||||
const bool hasHandshakeData = (last_handshake_time_sec >= 0);
|
||||
const bool hasFreshHandshake = hasHandshakeData &&
|
||||
((last_handshake_time_sec > 0) ||
|
||||
(rxBytes >= kHandshakeRxThreshold) ||
|
||||
(txBytes >= kHandshakeRxThreshold));
|
||||
|
||||
if (hasFreshHandshake) {
|
||||
m_handshakeConfirmed = true;
|
||||
m_handshakeAwaiting = false;
|
||||
m_handshakeTimer.invalidate();
|
||||
qDebug() << "IosController::checkStatus : handshake confirmed";
|
||||
emitConnectionStateIfChanged(Vpn::ConnectionState::Connected);
|
||||
} else if (m_handshakeTimer.isValid() &&
|
||||
m_handshakeTimer.elapsed() > kHandshakeTimeoutMs) {
|
||||
m_handshakeTimer.restart();
|
||||
qDebug() << "IosController::checkStatus : handshake timed out, keeping tunnel alive";
|
||||
emitConnectionStateIfChanged(Vpn::ConnectionState::Reconnecting);
|
||||
}
|
||||
}
|
||||
|
||||
emit bytesChanged(rxBytes - m_rxBytes, txBytes - m_txBytes);
|
||||
m_rxBytes = rxBytes;
|
||||
m_txBytes = txBytes;
|
||||
m_statusRequestInFlight = false;
|
||||
}, Qt::QueuedConnection);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -375,7 +496,22 @@ void IosController::vpnStatusDidChange(void *pNotification)
|
||||
}
|
||||
}
|
||||
|
||||
emit connectionStateChanged(iosStatusToState(session.status));
|
||||
Vpn::ConnectionState nextState = iosStatusToState(session.status);
|
||||
if (session.status == NEVPNStatusConnected && isWireGuardBasedProto(m_proto)) {
|
||||
if (!m_handshakeConfirmed) {
|
||||
nextState = Vpn::ConnectionState::Connecting;
|
||||
if (!m_handshakeAwaiting) {
|
||||
m_handshakeAwaiting = true;
|
||||
m_handshakeTimer.restart();
|
||||
}
|
||||
}
|
||||
} else if (session.status != NEVPNStatusConnected) {
|
||||
m_handshakeAwaiting = false;
|
||||
m_handshakeConfirmed = false;
|
||||
m_handshakeTimer.invalidate();
|
||||
m_statusRequestInFlight = false;
|
||||
}
|
||||
emitConnectionStateIfChanged(nextState);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -636,10 +772,6 @@ bool IosController::setupAwg()
|
||||
wgConfig.insert(config_key::specialJunk3, config[config_key::specialJunk3]);
|
||||
wgConfig.insert(config_key::specialJunk4, config[config_key::specialJunk4]);
|
||||
wgConfig.insert(config_key::specialJunk5, config[config_key::specialJunk5]);
|
||||
wgConfig.insert(config_key::controlledJunk1, config[config_key::controlledJunk1]);
|
||||
wgConfig.insert(config_key::controlledJunk2, config[config_key::controlledJunk2]);
|
||||
wgConfig.insert(config_key::controlledJunk3, config[config_key::controlledJunk3]);
|
||||
wgConfig.insert(config_key::specialHandshakeTimeout, config[config_key::specialHandshakeTimeout]);
|
||||
|
||||
QJsonDocument wgConfigDoc(wgConfig);
|
||||
QString wgConfigDocStr(wgConfigDoc.toJson(QJsonDocument::Compact));
|
||||
@@ -765,6 +897,9 @@ void IosController::sendVpnExtensionMessage(NSDictionary* message, std::function
|
||||
{
|
||||
if (!m_currentTunnel) {
|
||||
qDebug() << "Cannot set an extension callback without a tunnel manager";
|
||||
if (callback) {
|
||||
callback(nil);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -774,6 +909,9 @@ void IosController::sendVpnExtensionMessage(NSDictionary* message, std::function
|
||||
if (!data || error) {
|
||||
qDebug() << "Failed to serialize message to VpnExtension as JSON. Error:"
|
||||
<< [error.localizedDescription UTF8String];
|
||||
if (callback) {
|
||||
callback(nil);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -804,11 +942,18 @@ void IosController::sendVpnExtensionMessage(NSDictionary* message, std::function
|
||||
[session sendProviderMessage:data returnError:&sendError responseHandler:completionHandler];
|
||||
} else {
|
||||
qDebug() << "Method sendProviderMessage:responseHandler:error: does not exist";
|
||||
if (callback) {
|
||||
callback(nil);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (sendError) {
|
||||
qDebug() << "Failed to send message to VpnExtension. Error:"
|
||||
<< [sendError.localizedDescription UTF8String];
|
||||
if (callback) {
|
||||
callback(nil);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -879,6 +1024,135 @@ QString IosController::openFile() {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
void IosController::purchaseProduct(const QString &productId,
|
||||
std::function<void(bool success,
|
||||
const QString &transactionId,
|
||||
const QString &purchasedProductId,
|
||||
const QString &originalTransactionId,
|
||||
const QString &errorString)> &&callback)
|
||||
{
|
||||
qInfo().noquote() << "[IAP][IosController] purchaseProduct called" << productId;
|
||||
if (@available(iOS 15.0, macOS 12.0, *)) {
|
||||
StoreKitController *controller = [StoreKitController sharedInstance];
|
||||
__block auto cb = std::move(callback);
|
||||
[controller purchaseProduct:productId.toNSString() completion:^(BOOL s,
|
||||
NSString * _Nullable transactionId,
|
||||
NSString * _Nullable prodId,
|
||||
NSString * _Nullable originalTxId,
|
||||
NSError * _Nullable error) {
|
||||
const QString txId = QString::fromUtf8((transactionId ?: @"").UTF8String);
|
||||
const QString pId = QString::fromUtf8((prodId ?: @"").UTF8String);
|
||||
const QString origTxId = QString::fromUtf8((originalTxId ?: @"").UTF8String);
|
||||
const QString err = QString::fromUtf8((error.localizedDescription ?: @"").UTF8String);
|
||||
|
||||
qInfo().noquote() << "[IAP][IosController] purchase completion" << "success=" << s
|
||||
<< "transactionId=" << txId << "originalTransactionId=" << origTxId
|
||||
<< "productId=" << pId << "error=" << err;
|
||||
|
||||
if (cb) {
|
||||
cb(s, txId, pId, origTxId, err);
|
||||
}
|
||||
}];
|
||||
} else {
|
||||
if (callback) {
|
||||
callback(false, QString(), QString(), QString(), "StoreKit 2 requires iOS 15.0 or later");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IosController::restorePurchases(std::function<void(bool success,
|
||||
const QList<QVariantMap> &transactions,
|
||||
const QString &errorString)> &&callback)
|
||||
{
|
||||
if (@available(iOS 15.0, macOS 12.0, *)) {
|
||||
StoreKitController *controller = [StoreKitController sharedInstance];
|
||||
__block auto cb = std::move(callback);
|
||||
[controller restorePurchasesWithCompletion:^(BOOL s,
|
||||
NSArray<NSDictionary *> * _Nullable restoredTransactions,
|
||||
NSError * _Nullable error) {
|
||||
QString err;
|
||||
if (error) {
|
||||
err = QString::fromUtf8(error.localizedDescription.UTF8String);
|
||||
}
|
||||
QList<QVariantMap> transactions;
|
||||
for (NSDictionary *dict in restoredTransactions ?: @[]) {
|
||||
QVariantMap transaction;
|
||||
NSString *transactionId = dict[@"transactionId"];
|
||||
NSString *productId = dict[@"productId"];
|
||||
NSString *originalTransactionId = dict[@"originalTransactionId"];
|
||||
|
||||
if (transactionId) {
|
||||
transaction.insert(QStringLiteral("transactionId"), QString::fromUtf8(transactionId.UTF8String));
|
||||
}
|
||||
if (productId) {
|
||||
transaction.insert(QStringLiteral("productId"), QString::fromUtf8(productId.UTF8String));
|
||||
}
|
||||
if (originalTransactionId) {
|
||||
transaction.insert(QStringLiteral("originalTransactionId"),
|
||||
QString::fromUtf8(originalTransactionId.UTF8String));
|
||||
}
|
||||
transactions.push_back(transaction);
|
||||
}
|
||||
if (cb) {
|
||||
cb(s, transactions, err);
|
||||
}
|
||||
}];
|
||||
} else {
|
||||
if (callback) {
|
||||
callback(false, QList<QVariantMap>(), "StoreKit 2 requires iOS 15.0 or later");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IosController::fetchProducts(const QStringList &productIds,
|
||||
std::function<void(const QList<QVariantMap> &products,
|
||||
const QStringList &invalidIds,
|
||||
const QString &errorString)> &&callback)
|
||||
{
|
||||
if (@available(iOS 15.0, macOS 12.0, *)) {
|
||||
StoreKitController *controller = [StoreKitController sharedInstance];
|
||||
NSMutableSet<NSString *> *ids = [NSMutableSet setWithCapacity:productIds.size()];
|
||||
for (const auto &pid : productIds) {
|
||||
[ids addObject:pid.toNSString()];
|
||||
}
|
||||
__block auto cb = std::move(callback);
|
||||
|
||||
[controller fetchProductsWithIdentifiers:ids
|
||||
completion:^(NSArray<NSDictionary *> * _Nonnull products,
|
||||
NSArray<NSString *> * _Nonnull invalidIdentifiers,
|
||||
NSError * _Nullable error) {
|
||||
QList<QVariantMap> outProducts;
|
||||
for (NSDictionary *p in products) {
|
||||
QVariantMap m;
|
||||
m["productId"] = QString::fromUtf8([p[@"productId"] UTF8String]);
|
||||
m["title"] = QString::fromUtf8([p[@"title"] UTF8String]);
|
||||
m["description"] = QString::fromUtf8([p[@"description"] UTF8String]);
|
||||
m["price"] = QString::fromUtf8([p[@"price"] UTF8String]);
|
||||
m["currencyCode"] = QString::fromUtf8([p[@"currencyCode"] UTF8String]);
|
||||
outProducts.push_back(m);
|
||||
}
|
||||
|
||||
QStringList invalid;
|
||||
for (NSString *inv in invalidIdentifiers) {
|
||||
invalid.push_back(QString::fromUtf8(inv.UTF8String));
|
||||
}
|
||||
|
||||
QString err;
|
||||
if (error) {
|
||||
err = QString::fromUtf8(error.localizedDescription.UTF8String);
|
||||
}
|
||||
|
||||
if (cb) {
|
||||
cb(outProducts, invalid, err);
|
||||
}
|
||||
}];
|
||||
} else {
|
||||
if (callback) {
|
||||
callback(QList<QVariantMap>(), QStringList(), "StoreKit 2 requires iOS 15.0 or later");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IosController::requestInetAccess() {
|
||||
NSURL *url = [NSURL URLWithString:@"http://captive.apple.com/generate_204"];
|
||||
if (!url) {
|
||||
@@ -897,3 +1171,8 @@ void IosController::requestInetAccess() {
|
||||
}];
|
||||
[task resume];
|
||||
}
|
||||
|
||||
bool IosController::isTestFlight() {
|
||||
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
|
||||
return receiptURL && [[receiptURL lastPathComponent] isEqualToString:@"sandboxReceipt"];
|
||||
}
|
||||
@@ -34,6 +34,9 @@ void IOSNetworkWatcher::initialize() {
|
||||
});
|
||||
nw_path_monitor_start(m_networkMonitor);
|
||||
|
||||
// Call start() to initialize sleep/wake monitoring (will call MacOSNetworkWatcher::start() if this is macOS)
|
||||
this->start();
|
||||
|
||||
//TODO IMPL FOR AMNEZIA
|
||||
}
|
||||
|
||||
|
||||
@@ -165,7 +165,7 @@ bool LinuxRouteMonitor::rtmSendRoute(int action, int flags, int type,
|
||||
|
||||
if (rtm->rtm_type == RTN_THROW) {
|
||||
struct in_addr ip4;
|
||||
inet_pton(AF_INET, NetworkUtilities::getGatewayAndIface().toUtf8(), &ip4);
|
||||
inet_pton(AF_INET, NetworkUtilities::getGatewayAndIface().first.toUtf8(), &ip4);
|
||||
nlmsg_append_attr(nlmsg, sizeof(buf), RTA_GATEWAY, &ip4, sizeof(ip4));
|
||||
nlmsg_append_attr32(nlmsg, sizeof(buf), RTA_PRIORITY, 0);
|
||||
rtm->rtm_type = RTN_UNICAST;
|
||||
|
||||
@@ -143,12 +143,6 @@ bool WireguardUtilsLinux::addInterface(const InterfaceConfig& config) {
|
||||
for (const QString& key : config.m_specialJunk.keys()) {
|
||||
out << key.toLower() << "=" << config.m_specialJunk.value(key) << "\n";
|
||||
}
|
||||
for (const QString& key : config.m_controlledJunk.keys()) {
|
||||
out << key.toLower() << "=" << config.m_controlledJunk.value(key) << "\n";
|
||||
}
|
||||
if (!config.m_specialHandshakeTimeout.isEmpty()) {
|
||||
out << "itime=" << config.m_specialHandshakeTimeout << "\n";
|
||||
}
|
||||
|
||||
int err = uapiErrno(uapiCommand(message));
|
||||
if (err != 0) {
|
||||
|
||||
@@ -41,6 +41,9 @@ void LinuxNetworkWatcher::initialize() {
|
||||
connect(m_worker, &LinuxNetworkWatcherWorker::unsecuredNetwork, this,
|
||||
&LinuxNetworkWatcher::unsecuredNetwork);
|
||||
|
||||
connect(m_worker, &LinuxNetworkWatcherWorker::sleepMode, this,
|
||||
&NetworkWatcherImpl::sleepMode);
|
||||
|
||||
// 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
|
||||
// it makes the UI faster to appear, plus it gives a bit of delay between the
|
||||
|
||||
@@ -33,7 +33,21 @@
|
||||
#define NM_802_11_AP_SEC_WEAK_CRYPTO \
|
||||
(NM_802_11_AP_SEC_PAIR_WEP40 | NM_802_11_AP_SEC_PAIR_WEP104)
|
||||
|
||||
|
||||
enum NMState {
|
||||
NM_STATE_UNKNOWN = 0,
|
||||
NM_STATE_ASLEEP = 10,
|
||||
NM_STATE_DISCONNECTED = 20,
|
||||
NM_STATE_DISCONNECTING = 30,
|
||||
NM_STATE_CONNECTING = 40,
|
||||
NM_STATE_CONNECTED_LOCAL = 50,
|
||||
NM_STATE_CONNECTED_SITE = 60,
|
||||
NM_STATE_CONNECTED_GLOBAL = 70
|
||||
};
|
||||
|
||||
|
||||
constexpr const char* DBUS_NETWORKMANAGER = "org.freedesktop.NetworkManager";
|
||||
constexpr const char* DBUS_NETWORKMANAGER_PATH = "/org/freedesktop/NetworkManager";
|
||||
|
||||
namespace {
|
||||
Logger logger("LinuxNetworkWatcherWorker");
|
||||
@@ -73,7 +87,7 @@ void LinuxNetworkWatcherWorker::initialize() {
|
||||
// documentation:
|
||||
// https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.html
|
||||
|
||||
QDBusInterface nm(DBUS_NETWORKMANAGER, "/org/freedesktop/NetworkManager",
|
||||
QDBusInterface nm(DBUS_NETWORKMANAGER, DBUS_NETWORKMANAGER_PATH,
|
||||
DBUS_NETWORKMANAGER, QDBusConnection::systemBus());
|
||||
if (!nm.isValid()) {
|
||||
logger.error()
|
||||
@@ -108,6 +122,12 @@ void LinuxNetworkWatcherWorker::initialize() {
|
||||
SLOT(propertyChanged(QString, QVariantMap, QStringList)));
|
||||
}
|
||||
|
||||
QDBusConnection::systemBus().connect(DBUS_NETWORKMANAGER,
|
||||
DBUS_NETWORKMANAGER_PATH,
|
||||
DBUS_NETWORKMANAGER,
|
||||
"StateChanged",
|
||||
this, SLOT(NMStateChanged(quint32)));
|
||||
|
||||
if (m_devicePaths.isEmpty()) {
|
||||
logger.warning() << "No wifi devices found";
|
||||
return;
|
||||
@@ -173,5 +193,16 @@ void LinuxNetworkWatcherWorker::checkDevices() {
|
||||
emit unsecuredNetwork(ssid, bssid);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void LinuxNetworkWatcherWorker::NMStateChanged(quint32 state)
|
||||
{
|
||||
if (state == NM_STATE_ASLEEP) {
|
||||
emit sleepMode();
|
||||
}
|
||||
|
||||
logger.debug() << "NMStateChanged " << state;
|
||||
}
|
||||
|
||||
Loaded 100 of 279 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user