Compare commits
164 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b4c03ede8c | |||
| 2edd7de413 | |||
| f0da2b003f | |||
| 650c1c6ebb | |||
| 8dbded1624 | |||
| cebfcc846e | |||
| 4c18ceaa50 | |||
| ebe3a5dac6 | |||
| 92deee5f67 | |||
| a75bd0cf5e | |||
| 46f5b3894b | |||
| 493ee22883 | |||
| ad14847eb5 | |||
| cd50e0b8a5 | |||
| 78f504e35c | |||
| bf3d11e5c4 | |||
| 9a0222aee3 | |||
| f0f0f7c5be | |||
| 36b1a863bf | |||
| 4103c5bbcf | |||
| fa69da6d56 | |||
| aaf2c9ddeb | |||
| dbbc7119ec | |||
| c57162c4cc | |||
| 40e39895c9 | |||
| ec3ab2a03c | |||
| ddecfcad26 | |||
| 67bd880cdf | |||
| 477afb9d85 | |||
| f969fcdbb8 | |||
| b0ca16d861 | |||
| 9963359948 | |||
| ca639d293d | |||
| 83d045af64 | |||
| aea8ff4961 | |||
| 1892db4375 | |||
| c86a641e05 | |||
| befb2bf19a | |||
| 7ad6bc340c | |||
| 9164e38c34 | |||
| 8f7559f01b | |||
| af56200735 | |||
| 3874050fae | |||
| 3087163e34 | |||
| 1fa152845c | |||
| 50e23ef233 | |||
| ea648466de | |||
| b782775016 | |||
| 89a7fe1081 | |||
| e8bb096025 | |||
| fd5c7c8322 | |||
| e798d0f503 | |||
| bbb0abb596 | |||
| 0925aec86a | |||
| b084c4c284 | |||
| 87288ebccd | |||
| fcd7eadf4c | |||
| 0373338fb7 | |||
| 42f070fe9d | |||
| 02be6dc5f9 | |||
| bfcf7f0305 | |||
| 2bce595ade | |||
| cd1e561fd4 | |||
| 9bd1e6a0f5 | |||
| 5058c9aa6f | |||
| d78416835c | |||
| 40e6c6aae3 | |||
| 911a999c64 | |||
| b4f4184aa6 | |||
| 5c6db4b7a4 | |||
| f6277cdbb2 | |||
| 99312e61d3 | |||
| 9f0ae75a2f | |||
| 7960d8015d | |||
| 5dcc64e5e5 | |||
| 964436ad43 | |||
| 4fc3900fd5 | |||
| 8f5e42dd61 | |||
| 24895752c1 | |||
| 87eccfb4ca | |||
| a983d0504e | |||
| d0b8535395 | |||
| f84480cf56 | |||
| de7a026ec1 | |||
| a128c7d247 | |||
| f316f0e25a | |||
| ea5242e29b | |||
| b31a62c55f | |||
| 02e3107a23 | |||
| 1862850108 | |||
| f73792844c | |||
| a7199ca6f5 | |||
| 5e757cdd3b | |||
| 92af1f3268 | |||
| aad9d6dae2 | |||
| 423fe3fd4f | |||
| 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 |
@@ -10,13 +10,14 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
Build-Linux-Ubuntu:
|
Build-Linux-Ubuntu:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: android-runner
|
||||||
|
|
||||||
env:
|
env:
|
||||||
QT_VERSION: 6.6.2
|
QT_VERSION: 6.10.1
|
||||||
QIF_VERSION: 4.7
|
QIF_VERSION: 4.7
|
||||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||||
|
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
|
||||||
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
||||||
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||||
@@ -30,13 +31,15 @@ jobs:
|
|||||||
version: ${{ env.QT_VERSION }}
|
version: ${{ env.QT_VERSION }}
|
||||||
host: 'linux'
|
host: 'linux'
|
||||||
target: 'desktop'
|
target: 'desktop'
|
||||||
arch: 'gcc_64'
|
arch: 'linux_gcc_64'
|
||||||
modules: 'qtremoteobjects qt5compat qtshadertools'
|
modules: 'qtremoteobjects qt5compat qtshadertools'
|
||||||
dir: ${{ runner.temp }}
|
dir: ${{ runner.temp }}
|
||||||
setup-python: 'true'
|
setup-python: 'true'
|
||||||
tools: 'tools_ifw'
|
tools: 'tools_ifw'
|
||||||
set-env: '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'
|
- name: 'Get sources'
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -44,24 +47,31 @@ jobs:
|
|||||||
submodules: 'true'
|
submodules: 'true'
|
||||||
fetch-depth: 10
|
fetch-depth: 10
|
||||||
|
|
||||||
- name: 'Setup ccache'
|
- name: 'Get version from CMakeLists.txt'
|
||||||
uses: hendrikmuhs/ccache-action@v1.2
|
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'
|
- name: 'Build project'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get install libxkbcommon-x11-0
|
sudo apt-get install libxkbcommon-x11-0 libsecret-1-dev
|
||||||
export QT_BIN_DIR=${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/gcc_64/bin
|
export QT_BIN_DIR=${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/gcc_64/bin
|
||||||
export QIF_BIN_DIR=${{ runner.temp }}/Qt/Tools/QtInstallerFramework/${{ env.QIF_VERSION }}/bin
|
export QIF_BIN_DIR=${{ runner.temp }}/Qt/Tools/QtInstallerFramework/${{ env.QIF_VERSION }}/bin
|
||||||
bash deploy/build_linux.sh
|
bash deploy/build_linux.sh
|
||||||
|
|
||||||
- name: 'Pack installer'
|
- 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'
|
- name: 'Upload installer artifact'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: AmneziaVPN_Linux_installer.tar
|
name: AmneziaVPN_${{ env.VERSION }}_linux_x64.tar.zip
|
||||||
path: deploy/AmneziaVPN_Linux_Installer.tar
|
path: deploy/AmneziaVPN_${{ env.VERSION }}_linux_x64.tar.zip
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
- name: 'Upload unpacked artifact'
|
- name: 'Upload unpacked artifact'
|
||||||
@@ -84,11 +94,12 @@ jobs:
|
|||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
|
||||||
env:
|
env:
|
||||||
QT_VERSION: 6.6.2
|
QT_VERSION: 6.10.1
|
||||||
QIF_VERSION: 4.7
|
QIF_VERSION: 4.7
|
||||||
BUILD_ARCH: 64
|
BUILD_ARCH: 64
|
||||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||||
|
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
|
||||||
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
||||||
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||||
@@ -102,8 +113,16 @@ jobs:
|
|||||||
submodules: 'true'
|
submodules: 'true'
|
||||||
fetch-depth: 10
|
fetch-depth: 10
|
||||||
|
|
||||||
- name: 'Setup ccache'
|
- name: 'Get version from CMakeLists.txt'
|
||||||
uses: hendrikmuhs/ccache-action@v1.2
|
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'
|
- name: 'Install Qt'
|
||||||
uses: jurplel/install-qt-action@v3
|
uses: jurplel/install-qt-action@v3
|
||||||
@@ -111,32 +130,62 @@ jobs:
|
|||||||
version: ${{ env.QT_VERSION }}
|
version: ${{ env.QT_VERSION }}
|
||||||
host: 'windows'
|
host: 'windows'
|
||||||
target: 'desktop'
|
target: 'desktop'
|
||||||
arch: 'win64_msvc2019_64'
|
arch: 'win64_msvc2022_64'
|
||||||
modules: 'qtremoteobjects qt5compat qtshadertools'
|
modules: 'qtremoteobjects qt5compat qtshadertools'
|
||||||
dir: ${{ runner.temp }}
|
dir: ${{ runner.temp }}
|
||||||
setup-python: 'true'
|
setup-python: 'true'
|
||||||
tools: 'tools_ifw'
|
tools: 'tools_ifw'
|
||||||
set-env: 'true'
|
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'
|
- name: 'Setup mvsc'
|
||||||
uses: ilammy/msvc-dev-cmd@v1
|
uses: ilammy/msvc-dev-cmd@v1
|
||||||
with:
|
with:
|
||||||
arch: 'x64'
|
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'
|
- name: 'Build project'
|
||||||
shell: cmd
|
shell: cmd
|
||||||
run: |
|
run: |
|
||||||
set BUILD_ARCH=${{ env.BUILD_ARCH }}
|
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 QIF_BIN_DIR="${{ runner.temp }}\\Qt\\Tools\\QtInstallerFramework\\${{ env.QIF_VERSION }}\\bin"
|
||||||
|
set WIX_BIN_DIR=%USERPROFILE%\.dotnet\tools
|
||||||
call deploy\\build_windows.bat
|
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'
|
- name: 'Upload installer artifact'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: AmneziaVPN_Windows_installer
|
name: AmneziaVPN_${{ env.VERSION }}_x64.exe
|
||||||
path: AmneziaVPN_x${{ env.BUILD_ARCH }}.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
|
retention-days: 7
|
||||||
|
|
||||||
- name: 'Upload unpacked artifact'
|
- name: 'Upload unpacked artifact'
|
||||||
@@ -149,14 +198,15 @@ jobs:
|
|||||||
# ------------------------------------------------------
|
# ------------------------------------------------------
|
||||||
|
|
||||||
Build-iOS:
|
Build-iOS:
|
||||||
runs-on: macos-13
|
runs-on: macos-latest
|
||||||
|
|
||||||
env:
|
env:
|
||||||
QT_VERSION: 6.6.2
|
QT_VERSION: 6.10.1
|
||||||
CC: cc
|
CC: cc
|
||||||
CXX: c++
|
CXX: c++
|
||||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||||
|
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
|
||||||
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
||||||
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||||
@@ -167,7 +217,7 @@ jobs:
|
|||||||
- name: 'Setup xcode'
|
- name: 'Setup xcode'
|
||||||
uses: maxim-lobanov/setup-xcode@v1
|
uses: maxim-lobanov/setup-xcode@v1
|
||||||
with:
|
with:
|
||||||
xcode-version: '15.2'
|
xcode-version: '26.1'
|
||||||
|
|
||||||
- name: 'Install desktop Qt'
|
- name: 'Install desktop Qt'
|
||||||
uses: jurplel/install-qt-action@v3
|
uses: jurplel/install-qt-action@v3
|
||||||
@@ -211,8 +261,8 @@ jobs:
|
|||||||
submodules: 'true'
|
submodules: 'true'
|
||||||
fetch-depth: 10
|
fetch-depth: 10
|
||||||
|
|
||||||
- name: 'Setup ccache'
|
# - name: 'Setup ccache'
|
||||||
uses: hendrikmuhs/ccache-action@v1.2
|
# uses: hendrikmuhs/ccache-action@v1.2
|
||||||
|
|
||||||
- name: 'Install dependencies'
|
- name: 'Install dependencies'
|
||||||
run: pip install jsonschema jinja2
|
run: pip install jsonschema jinja2
|
||||||
@@ -271,6 +321,7 @@ jobs:
|
|||||||
|
|
||||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||||
|
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
|
||||||
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
||||||
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||||
@@ -303,8 +354,8 @@ jobs:
|
|||||||
submodules: 'true'
|
submodules: 'true'
|
||||||
fetch-depth: 10
|
fetch-depth: 10
|
||||||
|
|
||||||
- name: 'Setup ccache'
|
# - name: 'Setup ccache'
|
||||||
uses: hendrikmuhs/ccache-action@v1.2
|
# uses: hendrikmuhs/ccache-action@v1.2
|
||||||
|
|
||||||
- name: 'Build project'
|
- name: 'Build project'
|
||||||
run: |
|
run: |
|
||||||
@@ -331,7 +382,7 @@ jobs:
|
|||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
|
|
||||||
env:
|
env:
|
||||||
QT_VERSION: 6.8.0
|
QT_VERSION: 6.10.1
|
||||||
|
|
||||||
MAC_TEAM_ID: ${{ secrets.MAC_TEAM_ID }}
|
MAC_TEAM_ID: ${{ secrets.MAC_TEAM_ID }}
|
||||||
|
|
||||||
@@ -348,6 +399,7 @@ jobs:
|
|||||||
|
|
||||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||||
|
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
|
||||||
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
||||||
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||||
@@ -361,7 +413,7 @@ jobs:
|
|||||||
xcode-version: '16.2.0'
|
xcode-version: '16.2.0'
|
||||||
|
|
||||||
- name: 'Install Qt'
|
- name: 'Install Qt'
|
||||||
uses: jurplel/install-qt-action@v3
|
uses: jurplel/install-qt-action@v4
|
||||||
with:
|
with:
|
||||||
version: ${{ env.QT_VERSION }}
|
version: ${{ env.QT_VERSION }}
|
||||||
host: 'mac'
|
host: 'mac'
|
||||||
@@ -371,8 +423,9 @@ jobs:
|
|||||||
dir: ${{ runner.temp }}
|
dir: ${{ runner.temp }}
|
||||||
setup-python: 'true'
|
setup-python: 'true'
|
||||||
set-env: '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'
|
- name: 'Get sources'
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -380,19 +433,32 @@ jobs:
|
|||||||
submodules: 'true'
|
submodules: 'true'
|
||||||
fetch-depth: 10
|
fetch-depth: 10
|
||||||
|
|
||||||
- name: 'Setup ccache'
|
- name: 'Get version from CMakeLists.txt'
|
||||||
uses: hendrikmuhs/ccache-action@v1.2
|
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'
|
- name: 'Build project'
|
||||||
run: |
|
run: |
|
||||||
export QT_BIN_DIR="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/macos/bin"
|
export QT_BIN_DIR="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/macos/bin"
|
||||||
bash deploy/build_macos.sh -n
|
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'
|
- name: 'Upload installer artifact'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: AmneziaVPN_MacOS_installer
|
name: AmneziaVPN_${{ env.VERSION }}_macos.zip
|
||||||
path: deploy/build/pkg/AmneziaVPN.pkg
|
path: deploy/AmneziaVPN_${{ env.VERSION }}_macos.zip
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
- name: 'Upload unpacked artifact'
|
- name: 'Upload unpacked artifact'
|
||||||
@@ -406,7 +472,7 @@ jobs:
|
|||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
|
|
||||||
env:
|
env:
|
||||||
QT_VERSION: 6.8.3
|
QT_VERSION: 6.10.1
|
||||||
|
|
||||||
MAC_TEAM_ID: ${{ secrets.MAC_TEAM_ID }}
|
MAC_TEAM_ID: ${{ secrets.MAC_TEAM_ID }}
|
||||||
|
|
||||||
@@ -416,6 +482,7 @@ jobs:
|
|||||||
|
|
||||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||||
|
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
|
||||||
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
||||||
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||||
@@ -426,21 +493,31 @@ jobs:
|
|||||||
- name: 'Setup xcode'
|
- name: 'Setup xcode'
|
||||||
uses: maxim-lobanov/setup-xcode@v1
|
uses: maxim-lobanov/setup-xcode@v1
|
||||||
with:
|
with:
|
||||||
xcode-version: '16.2.0'
|
xcode-version: '26.1'
|
||||||
|
|
||||||
- name: 'Install Qt'
|
- name: 'Install desktop Qt'
|
||||||
uses: jurplel/install-qt-action@v3
|
uses: jurplel/install-qt-action@v3
|
||||||
with:
|
with:
|
||||||
version: ${{ env.QT_VERSION }}
|
version: ${{ env.QT_VERSION }}
|
||||||
host: 'mac'
|
host: 'mac'
|
||||||
target: 'desktop'
|
target: 'desktop'
|
||||||
|
modules: 'qtremoteobjects qt5compat qtshadertools qtmultimedia'
|
||||||
arch: 'clang_64'
|
arch: 'clang_64'
|
||||||
modules: 'qtremoteobjects qt5compat qtshadertools'
|
|
||||||
dir: ${{ runner.temp }}
|
dir: ${{ runner.temp }}
|
||||||
setup-python: 'true'
|
|
||||||
set-env: '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'
|
- name: 'Get sources'
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -448,8 +525,8 @@ jobs:
|
|||||||
submodules: 'true'
|
submodules: 'true'
|
||||||
fetch-depth: 10
|
fetch-depth: 10
|
||||||
|
|
||||||
- name: 'Setup ccache'
|
# - name: 'Setup ccache'
|
||||||
uses: hendrikmuhs/ccache-action@v1.2
|
# uses: hendrikmuhs/ccache-action@v1.2
|
||||||
|
|
||||||
- name: 'Build project'
|
- name: 'Build project'
|
||||||
run: |
|
run: |
|
||||||
@@ -466,14 +543,15 @@ jobs:
|
|||||||
# ------------------------------------------------------
|
# ------------------------------------------------------
|
||||||
|
|
||||||
Build-Android:
|
Build-Android:
|
||||||
runs-on: ubuntu-latest
|
runs-on: android-runner
|
||||||
|
|
||||||
env:
|
env:
|
||||||
ANDROID_BUILD_PLATFORM: android-34
|
ANDROID_BUILD_PLATFORM: android-36
|
||||||
QT_VERSION: 6.7.3
|
QT_VERSION: 6.11.0
|
||||||
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
|
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
|
||||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||||
|
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
|
||||||
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
||||||
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||||
@@ -551,15 +629,22 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
submodules: 'true'
|
submodules: 'true'
|
||||||
|
|
||||||
- name: 'Setup ccache'
|
- name: 'Get version from CMakeLists.txt'
|
||||||
uses: hendrikmuhs/ccache-action@v1.2
|
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'
|
- name: 'Setup Java'
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
cache: 'gradle'
|
# cache: 'gradle'
|
||||||
|
|
||||||
- name: 'Setup Android NDK'
|
- name: 'Setup Android NDK'
|
||||||
id: setup-ndk
|
id: setup-ndk
|
||||||
@@ -584,35 +669,44 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: ./deploy/build_android.sh --aab --apk all --build-platform ${{ env.ANDROID_BUILD_PLATFORM }}
|
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'
|
- name: 'Upload x86_64 apk'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: AmneziaVPN-android-x86_64
|
name: AmneziaVPN_${{ env.VERSION }}_android9+_x86_64.apk
|
||||||
path: deploy/build/AmneziaVPN-x86_64-release.apk
|
path: deploy/build/AmneziaVPN_${{ env.VERSION }}_android9+_x86_64.apk
|
||||||
compression-level: 0
|
compression-level: 0
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
- name: 'Upload x86 apk'
|
- name: 'Upload x86 apk'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: AmneziaVPN-android-x86
|
name: AmneziaVPN_${{ env.VERSION }}_android9+_x86.apk
|
||||||
path: deploy/build/AmneziaVPN-x86-release.apk
|
path: deploy/build/AmneziaVPN_${{ env.VERSION }}_android9+_x86.apk
|
||||||
compression-level: 0
|
compression-level: 0
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
- name: 'Upload arm64-v8a apk'
|
- name: 'Upload arm64-v8a apk'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: AmneziaVPN-android-arm64-v8a
|
name: AmneziaVPN_${{ env.VERSION }}_android9+_arm64-v8a.apk
|
||||||
path: deploy/build/AmneziaVPN-arm64-v8a-release.apk
|
path: deploy/build/AmneziaVPN_${{ env.VERSION }}_android9+_arm64-v8a.apk
|
||||||
compression-level: 0
|
compression-level: 0
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
- name: 'Upload armeabi-v7a apk'
|
- name: 'Upload armeabi-v7a apk'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: AmneziaVPN-android-armeabi-v7a
|
name: AmneziaVPN_${{ env.VERSION }}_android9+_armeabi-v7a.apk
|
||||||
path: deploy/build/AmneziaVPN-armeabi-v7a-release.apk
|
path: deploy/build/AmneziaVPN_${{ env.VERSION }}_android9+_armeabi-v7a.apk
|
||||||
compression-level: 0
|
compression-level: 0
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ jobs:
|
|||||||
QIF_VERSION: 4.5
|
QIF_VERSION: 4.5
|
||||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||||
|
FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }}
|
||||||
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
||||||
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
- name: Verify git tag
|
- name: Verify git tag
|
||||||
run: |
|
run: |
|
||||||
TAG_NAME=${{ inputs.RELEASE_VERSION }}
|
TAG_NAME=${{ inputs.RELEASE_VERSION }}
|
||||||
CMAKE_TAG=$(grep 'project.*VERSION' CMakeLists.txt | sed -E 's/.* ([0-9]+.[0-9]+.[0-9]+.[0-9]+)$/\1/')
|
CMAKE_TAG=$(grep 'set(AMNEZIAVPN_VERSION' CMakeLists.txt | sed -E 's/.*AMNEZIAVPN_VERSION ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+).*/\1/')
|
||||||
if [[ "$TAG_NAME" == "$CMAKE_TAG" ]]; then
|
if [[ "$TAG_NAME" == "$CMAKE_TAG" ]]; then
|
||||||
echo "Git tag ($TAG_NAME) matches CMakeLists.txt version ($CMAKE_TAG)."
|
echo "Git tag ($TAG_NAME) matches CMakeLists.txt version ($CMAKE_TAG)."
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ deploy/build_32/*
|
|||||||
deploy/build_64/*
|
deploy/build_64/*
|
||||||
winbuild*.bat
|
winbuild*.bat
|
||||||
.cache/
|
.cache/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
|
||||||
# Qt-es
|
# Qt-es
|
||||||
@@ -139,3 +140,6 @@ ios-ne-build.sh
|
|||||||
macos-ne-build.sh
|
macos-ne-build.sh
|
||||||
macos-signed-build.sh
|
macos-signed-build.sh
|
||||||
macos-with-sign-build.sh
|
macos-with-sign-build.sh
|
||||||
|
DeveloperIdApplicationCertificate.p12
|
||||||
|
DeveloperIdInstallerCertificate.p12
|
||||||
|
|
||||||
|
|||||||
@@ -14,3 +14,7 @@
|
|||||||
[submodule "client/3rd/QSimpleCrypto"]
|
[submodule "client/3rd/QSimpleCrypto"]
|
||||||
path = client/3rd/QSimpleCrypto
|
path = client/3rd/QSimpleCrypto
|
||||||
url = https://github.com/amnezia-vpn/QSimpleCrypto.git
|
url = https://github.com/amnezia-vpn/QSimpleCrypto.git
|
||||||
|
[submodule "client/3rd/qtgamepad"]
|
||||||
|
path = client/3rd/qtgamepad
|
||||||
|
url = https://github.com/amnezia-vpn/qtgamepad.git
|
||||||
|
branch = 6.6
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
|
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
|
||||||
|
|
||||||
set(PROJECT AmneziaVPN)
|
set(PROJECT AmneziaVPN)
|
||||||
set(AMNEZIAVPN_VERSION 4.8.11.0)
|
set(AMNEZIAVPN_VERSION 4.8.15.5)
|
||||||
|
|
||||||
project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
|
project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
|
||||||
DESCRIPTION "AmneziaVPN"
|
DESCRIPTION "AmneziaVPN"
|
||||||
@@ -12,7 +12,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
|
|||||||
set(RELEASE_DATE "${CURRENT_DATE}")
|
set(RELEASE_DATE "${CURRENT_DATE}")
|
||||||
|
|
||||||
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
|
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
|
||||||
set(APP_ANDROID_VERSION_CODE 2095)
|
set(APP_ANDROID_VERSION_CODE 2121)
|
||||||
|
|
||||||
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||||
set(MZ_PLATFORM_NAME "linux")
|
set(MZ_PLATFORM_NAME "linux")
|
||||||
@@ -49,3 +49,39 @@ if(NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
|
|||||||
|
|
||||||
include(${CMAKE_SOURCE_DIR}/deploy/installer/config.cmake)
|
include(${CMAKE_SOURCE_DIR}/deploy/installer/config.cmake)
|
||||||
endif()
|
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(AMNEZIA_LICENSE_TXT "${CMAKE_BINARY_DIR}/LICENSE.txt")
|
||||||
|
configure_file("${CMAKE_SOURCE_DIR}/LICENSE" "${AMNEZIA_LICENSE_TXT}" COPYONLY)
|
||||||
|
set(CPACK_RESOURCE_FILE_LICENSE "${AMNEZIA_LICENSE_TXT}")
|
||||||
|
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()
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ You may face compiling issues in QT Creator after you've worked in Android Studi
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
GPL v3.0
|
This project is licensed under the GNU General Public License v3.0 (see LICENSE) and also includes third-party components distributed under their own terms (see THIRD_PARTY_LICENSES.md).
|
||||||
|
|
||||||
## Donate
|
## Donate
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
# Third-Party Licenses
|
||||||
|
|
||||||
|
This project is licensed under the GNU General Public License v3.0.
|
||||||
|
This file lists third-party software components used by this repository.
|
||||||
|
Each component is distributed under its own license as linked below.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## QtKeychain
|
||||||
|
|
||||||
|
- Source: https://github.com/frankosterfeld/qtkeychain
|
||||||
|
- License: BSD License
|
||||||
|
- License Text: https://www.gnu.org/licenses/license-list.html#ModifiedBSD
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## QSimpleCrypto
|
||||||
|
|
||||||
|
- Source: https://github.com/n1flh31mur/QSimpleCrypto
|
||||||
|
- License: Apache License 2.0
|
||||||
|
- License Text: https://github.com/n1flh31mur/QSimpleCrypto/blob/master/LICENSE
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SortFilterProxyModel
|
||||||
|
|
||||||
|
- Source: https://github.com/oKcerG/SortFilterProxyModel
|
||||||
|
- License: MIT License
|
||||||
|
- License Text: https://github.com/oKcerG/SortFilterProxyModel/blob/master/LICENSE
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## QJsonStruct
|
||||||
|
|
||||||
|
- Source: https://github.com/Qv2ray/QJsonStruct
|
||||||
|
- License: MIT License
|
||||||
|
- License Text: https://github.com/Qv2ray/QJsonStruct/blob/master/LICENSE
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## QR Code Generator (qrcodegen)
|
||||||
|
|
||||||
|
- Source: https://github.com/nayuki/QR-Code-generator
|
||||||
|
- License: MIT License
|
||||||
|
- License Text: https://www.nayuki.io/page/qr-code-generator-library
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Qt Gamepad
|
||||||
|
|
||||||
|
- Source: https://github.com/qt/qtgamepad
|
||||||
|
- License: GNU General Public License v3.0 (GPL-3.0)
|
||||||
|
- License Text: https://www.gnu.org/licenses/gpl-3.0.en.html
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AmneziaWG Apple (WireGuard)
|
||||||
|
|
||||||
|
- Source: https://github.com/amnezia-vpn/amneziawg-apple
|
||||||
|
- License: MIT License
|
||||||
|
- License Text: https://github.com/amnezia-vpn/amneziawg-apple/blob/master/COPYING
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AmneziaWG Android
|
||||||
|
|
||||||
|
- Source: https://github.com/amnezia-vpn/amneziawg-go
|
||||||
|
- License: MIT License
|
||||||
|
- License Text: https://github.com/amnezia-vpn/amneziawg-go/blob/master/LICENSE
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Xray Core
|
||||||
|
|
||||||
|
- Source: https://github.com/XTLS/Xray-core
|
||||||
|
- License: Mozilla Public License 2.0 (MPL-2.0)
|
||||||
|
- License Text: https://github.com/XTLS/Xray-core/blob/main/LICENSE
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cloak
|
||||||
|
|
||||||
|
- Source: https://github.com/cbeuw/Cloak
|
||||||
|
- License: GNU General Public License v3.0 (GPL-3.0)
|
||||||
|
- License Text: https://github.com/cbeuw/Cloak/blob/master/LICENSE
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Shadowsocks
|
||||||
|
|
||||||
|
- Source: https://github.com/shadowsocks/shadowsocks-libev
|
||||||
|
- License: GPL-3.0-or-later
|
||||||
|
- License Text: http://www.gnu.org/licenses/
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## OpenSSL
|
||||||
|
|
||||||
|
- Source: https://github.com/openssl/openssl
|
||||||
|
- License: Apache License 2.0
|
||||||
|
- License Text: https://www.openssl.org/source/license.html
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## libssh
|
||||||
|
|
||||||
|
- Source: https://www.libssh.org/
|
||||||
|
- License: GNU Lesser General Public License (LGPL)
|
||||||
|
- License Text: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## OpenVPNAdapter
|
||||||
|
|
||||||
|
- Source: https://github.com/ss-abramchuk/OpenVPNAdapter
|
||||||
|
- License: GNU Affero General Public License v3.0 (AGPL-3.0)
|
||||||
|
- License Text: https://github.com/ss-abramchuk/OpenVPNAdapter/blob/master/LICENSE
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Wintun
|
||||||
|
|
||||||
|
- Source: https://www.wintun.net/
|
||||||
|
- License: Prebuilt Binaries License
|
||||||
|
- License Text: https://github.com/WireGuard/wintun/blob/master/prebuilt-binaries-license.txt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mullvad Split Tunnel Driver
|
||||||
|
|
||||||
|
- Source: https://github.com/mullvad/win-split-tunnel
|
||||||
|
- License: GNU General Public License v3.0 (GPL-3.0) and Mozilla Public License Version 2.0
|
||||||
|
- License Text: https://github.com/mullvad/win-split-tunnel/blob/master/LICENSE-GPL.md https://github.com/mullvad/win-split-tunnel/blob/master/LICENSE-MPL.txt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## tun2socks
|
||||||
|
|
||||||
|
- Source: https://github.com/eycorsican/go-tun2socks
|
||||||
|
- License: MIT License
|
||||||
|
- License Text: https://github.com/eycorsican/go-tun2socks/blob/master/LICENSE
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TAP-Windows Driver
|
||||||
|
|
||||||
|
- Source: https://github.com/OpenVPN/tap-windows6
|
||||||
|
- License: tap-windows6 license
|
||||||
|
- License Text: https://github.com/OpenVPN/tap-windows6/blob/master/COPYING
|
||||||
@@ -25,6 +25,7 @@ add_definitions(-DGIT_COMMIT_HASH="${GIT_COMMIT_HASH}")
|
|||||||
|
|
||||||
add_definitions(-DPROD_AGW_PUBLIC_KEY="$ENV{PROD_AGW_PUBLIC_KEY}")
|
add_definitions(-DPROD_AGW_PUBLIC_KEY="$ENV{PROD_AGW_PUBLIC_KEY}")
|
||||||
add_definitions(-DPROD_S3_ENDPOINT="$ENV{PROD_S3_ENDPOINT}")
|
add_definitions(-DPROD_S3_ENDPOINT="$ENV{PROD_S3_ENDPOINT}")
|
||||||
|
add_definitions(-DFALLBACK_S3_ENDPOINT="$ENV{FALLBACK_S3_ENDPOINT}")
|
||||||
|
|
||||||
add_definitions(-DDEV_AGW_PUBLIC_KEY="$ENV{DEV_AGW_PUBLIC_KEY}")
|
add_definitions(-DDEV_AGW_PUBLIC_KEY="$ENV{DEV_AGW_PUBLIC_KEY}")
|
||||||
add_definitions(-DDEV_AGW_ENDPOINT="$ENV{DEV_AGW_ENDPOINT}")
|
add_definitions(-DDEV_AGW_ENDPOINT="$ENV{DEV_AGW_ENDPOINT}")
|
||||||
@@ -56,10 +57,9 @@ target_include_directories(${PROJECT} PUBLIC
|
|||||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>
|
$<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_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_interface.rep)
|
||||||
qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_process_tun2socks.rep)
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
qt6_add_resources(QRC ${QRC} ${CMAKE_CURRENT_LIST_DIR}/resources.qrc)
|
qt6_add_resources(QRC ${QRC} ${CMAKE_CURRENT_LIST_DIR}/resources.qrc)
|
||||||
@@ -79,6 +79,7 @@ set(AMNEZIAVPN_TS_FILES
|
|||||||
)
|
)
|
||||||
|
|
||||||
file(GLOB_RECURSE AMNEZIAVPN_TS_SOURCES *.qrc *.cpp *.h *.ui)
|
file(GLOB_RECURSE AMNEZIAVPN_TS_SOURCES *.qrc *.cpp *.h *.ui)
|
||||||
|
list(FILTER AMNEZIAVPN_TS_SOURCES EXCLUDE REGEX "qtgamepad/examples")
|
||||||
|
|
||||||
qt_create_translation(AMNEZIAVPN_QM_FILES ${AMNEZIAVPN_TS_SOURCES} ${AMNEZIAVPN_TS_FILES})
|
qt_create_translation(AMNEZIAVPN_QM_FILES ${AMNEZIAVPN_TS_SOURCES} ${AMNEZIAVPN_TS_FILES})
|
||||||
|
|
||||||
@@ -228,4 +229,13 @@ if(NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
target_sources(${PROJECT} PRIVATE ${SOURCES} ${HEADERS} ${RESOURCES} ${QRC} ${I18NQRC})
|
target_sources(${PROJECT} PRIVATE ${SOURCES} ${HEADERS} ${RESOURCES} ${QRC} ${I18NQRC})
|
||||||
qt_finalize_target(${PROJECT})
|
|
||||||
|
# Finalize the executable so Qt can gather/deploy QML modules and plugins correctly (Android needs this).
|
||||||
|
if(COMMAND qt_import_qml_plugins)
|
||||||
|
qt_import_qml_plugins(${PROJECT})
|
||||||
|
endif()
|
||||||
|
if(COMMAND qt_finalize_executable)
|
||||||
|
qt_finalize_executable(${PROJECT})
|
||||||
|
else()
|
||||||
|
qt_finalize_target(${PROJECT})
|
||||||
|
endif()
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QTranslator>
|
#include <QTranslator>
|
||||||
#include <QEvent>
|
#include <QEvent>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QSettings>
|
||||||
|
|
||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
#include "ui/controllers/pageController.h"
|
#include "ui/controllers/pageController.h"
|
||||||
@@ -25,10 +27,15 @@
|
|||||||
#include <QtQuick/QQuickWindow> // for QQuickWindow
|
#include <QtQuick/QQuickWindow> // for QQuickWindow
|
||||||
#include <QWindow> // for qobject_cast<QWindow*>
|
#include <QWindow> // for qobject_cast<QWindow*>
|
||||||
|
|
||||||
|
bool AmneziaApplication::m_forceQuit = false;
|
||||||
|
|
||||||
AmneziaApplication::AmneziaApplication(int &argc, char *argv[]) : AMNEZIA_BASE_CLASS(argc, argv),
|
AmneziaApplication::AmneziaApplication(int &argc, char *argv[]) : AMNEZIA_BASE_CLASS(argc, argv),
|
||||||
m_optAutostart({QStringLiteral("a"), QStringLiteral("autostart")}, QStringLiteral("System autostart")),
|
m_optAutostart({QStringLiteral("a"), QStringLiteral("autostart")}, QStringLiteral("System autostart")),
|
||||||
m_optCleanup ({QStringLiteral("c"), QStringLiteral("cleanup")}, QStringLiteral("Cleanup logs"))
|
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);
|
setQuitOnLastWindowClosed(false);
|
||||||
|
|
||||||
// Fix config file permissions
|
// Fix config file permissions
|
||||||
@@ -53,14 +60,40 @@ AmneziaApplication::AmneziaApplication(int &argc, char *argv[]) : AMNEZIA_BASE_C
|
|||||||
|
|
||||||
AmneziaApplication::~AmneziaApplication()
|
AmneziaApplication::~AmneziaApplication()
|
||||||
{
|
{
|
||||||
|
#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();
|
m_vpnConnectionThread.quit();
|
||||||
|
|
||||||
|
if (!m_vpnConnectionThread.wait(3000)) {
|
||||||
|
m_vpnConnectionThread.terminate();
|
||||||
|
m_vpnConnectionThread.wait(500);
|
||||||
|
}
|
||||||
|
|
||||||
if (m_engine) {
|
if (m_engine) {
|
||||||
QObject::disconnect(m_engine, 0, 0, 0);
|
|
||||||
delete m_engine;
|
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()
|
void AmneziaApplication::init()
|
||||||
{
|
{
|
||||||
m_engine = new QQmlApplicationEngine;
|
m_engine = new QQmlApplicationEngine;
|
||||||
@@ -76,6 +109,16 @@ void AmneziaApplication::init()
|
|||||||
// install filter on main window
|
// install filter on main window
|
||||||
if (auto win = qobject_cast<QQuickWindow*>(obj)) {
|
if (auto win = qobject_cast<QQuickWindow*>(obj)) {
|
||||||
win->installEventFilter(this);
|
win->installEventFilter(this);
|
||||||
|
#ifdef Q_OS_ANDROID
|
||||||
|
QObject::connect(win, &QQuickWindow::sceneGraphError,
|
||||||
|
[](QQuickWindow::SceneGraphError, const QString &msg) {
|
||||||
|
qWarning() << "Scene graph error (suppressed):" << msg;
|
||||||
|
});
|
||||||
|
// Keep graphics context alive across hide/show cycles to avoid
|
||||||
|
// eglSwapBuffers/makeCurrent being called on a context Android has reclaimed.
|
||||||
|
win->setPersistentSceneGraph(true);
|
||||||
|
win->setPersistentGraphics(true);
|
||||||
|
#endif
|
||||||
win->show();
|
win->show();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -96,6 +139,16 @@ void AmneziaApplication::init()
|
|||||||
m_coreController.reset(new CoreController(m_vpnConnection, m_settings, m_engine));
|
m_coreController.reset(new CoreController(m_vpnConnection, m_settings, m_engine));
|
||||||
|
|
||||||
m_engine->addImportPath("qrc:/ui/qml/Modules/");
|
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_engine->load(url);
|
||||||
|
|
||||||
m_coreController->setQmlRoot();
|
m_coreController->setQmlRoot();
|
||||||
@@ -135,6 +188,18 @@ void AmneziaApplication::init()
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
#endif
|
#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()
|
void AmneziaApplication::registerTypes()
|
||||||
@@ -181,6 +246,8 @@ bool AmneziaApplication::parseCommands()
|
|||||||
|
|
||||||
m_parser.addOption(m_optAutostart);
|
m_parser.addOption(m_optAutostart);
|
||||||
m_parser.addOption(m_optCleanup);
|
m_parser.addOption(m_optCleanup);
|
||||||
|
m_parser.addOption(m_optConnect);
|
||||||
|
m_parser.addOption(m_optImport);
|
||||||
|
|
||||||
m_parser.process(*this);
|
m_parser.process(*this);
|
||||||
|
|
||||||
@@ -217,8 +284,12 @@ bool AmneziaApplication::eventFilter(QObject *watched, QEvent *event)
|
|||||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||||
quit();
|
quit();
|
||||||
#else
|
#else
|
||||||
if (m_coreController && m_coreController->pageController()) {
|
if (m_forceQuit) {
|
||||||
m_coreController->pageController()->hideMainWindow();
|
quit();
|
||||||
|
} else {
|
||||||
|
if (m_coreController && m_coreController->pageController()) {
|
||||||
|
m_coreController->pageController()->hideMainWindow();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
return true; // eat the close
|
return true; // eat the close
|
||||||
@@ -227,6 +298,12 @@ bool AmneziaApplication::eventFilter(QObject *watched, QEvent *event)
|
|||||||
return QObject::eventFilter(watched, event);
|
return QObject::eventFilter(watched, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AmneziaApplication::forceQuit()
|
||||||
|
{
|
||||||
|
m_forceQuit = true;
|
||||||
|
quit();
|
||||||
|
}
|
||||||
|
|
||||||
QQmlApplicationEngine *AmneziaApplication::qmlEngine() const
|
QQmlApplicationEngine *AmneziaApplication::qmlEngine() const
|
||||||
{
|
{
|
||||||
return m_engine;
|
return m_engine;
|
||||||
|
|||||||
@@ -45,7 +45,11 @@ public:
|
|||||||
QNetworkAccessManager *networkManager();
|
QNetworkAccessManager *networkManager();
|
||||||
QClipboard *getClipboard();
|
QClipboard *getClipboard();
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void forceQuit();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
static bool m_forceQuit;
|
||||||
QQmlApplicationEngine *m_engine {};
|
QQmlApplicationEngine *m_engine {};
|
||||||
std::shared_ptr<Settings> m_settings;
|
std::shared_ptr<Settings> m_settings;
|
||||||
|
|
||||||
@@ -58,6 +62,8 @@ private:
|
|||||||
|
|
||||||
QCommandLineOption m_optAutostart;
|
QCommandLineOption m_optAutostart;
|
||||||
QCommandLineOption m_optCleanup;
|
QCommandLineOption m_optCleanup;
|
||||||
|
QCommandLineOption m_optConnect;
|
||||||
|
QCommandLineOption m_optImport;
|
||||||
|
|
||||||
QSharedPointer<VpnConnection> m_vpnConnection;
|
QSharedPointer<VpnConnection> m_vpnConnection;
|
||||||
QThread m_vpnConnectionThread;
|
QThread m_vpnConnectionThread;
|
||||||
|
|||||||
@@ -45,7 +45,8 @@
|
|||||||
android:configChanges="uiMode|screenSize|smallestScreenSize|screenLayout|orientation|density
|
android:configChanges="uiMode|screenSize|smallestScreenSize|screenLayout|orientation|density
|
||||||
|fontScale|layoutDirection|locale|keyboard|keyboardHidden|navigation|mcc|mnc"
|
|fontScale|layoutDirection|locale|keyboard|keyboardHidden|navigation|mcc|mnc"
|
||||||
android:launchMode="singleInstance"
|
android:launchMode="singleInstance"
|
||||||
android:windowSoftInputMode="stateUnchanged|adjustResize"
|
android:windowSoftInputMode="adjustResize|stateUnchanged"
|
||||||
|
android:enableOnBackInvokedCallback="false"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@@ -214,4 +215,4 @@
|
|||||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/qtprovider_paths" />
|
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/qtprovider_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -93,7 +93,7 @@ open class OpenVpn : Protocol() {
|
|||||||
openVpnClient = null
|
openVpnClient = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun reconnectVpn(vpnBuilder: Builder) {
|
override fun reconnectVpn(vpnBuilder: Builder, protect: (Int) -> Boolean) {
|
||||||
openVpnClient?.let {
|
openVpnClient?.let {
|
||||||
it.establish = makeEstablish(vpnBuilder)
|
it.establish = makeEstablish(vpnBuilder)
|
||||||
it.reconnect(0)
|
it.reconnect(0)
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ abstract class Protocol {
|
|||||||
|
|
||||||
abstract fun stopVpn()
|
abstract fun stopVpn()
|
||||||
|
|
||||||
abstract fun reconnectVpn(vpnBuilder: Builder)
|
abstract fun reconnectVpn(vpnBuilder: Builder, protect: (Int) -> Boolean)
|
||||||
|
|
||||||
protected fun ProtocolConfig.Builder.configSplitTunneling(config: JSONObject) {
|
protected fun ProtocolConfig.Builder.configSplitTunneling(config: JSONObject) {
|
||||||
if (!allowSplitTunneling) {
|
if (!allowSplitTunneling) {
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
<item name="android:colorBackground">@color/black</item>
|
<item name="android:colorBackground">@color/black</item>
|
||||||
<item name="android:windowActionBar">false</item>
|
<item name="android:windowActionBar">false</item>
|
||||||
<item name="android:windowNoTitle">true</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>
|
||||||
<style name="Translucent" parent="NoActionBar">
|
<style name="Translucent" parent="NoActionBar">
|
||||||
<item name="android:windowBackground">@android:color/transparent</item>
|
<item name="android:windowBackground">@android:color/transparent</item>
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import android.os.ParcelFileDescriptor
|
|||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
import android.view.InputDevice
|
||||||
|
import android.view.KeyEvent
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@@ -35,6 +37,11 @@ import android.widget.Toast
|
|||||||
import androidx.annotation.MainThread
|
import androidx.annotation.MainThread
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.content.ContextCompat
|
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 java.io.IOException
|
||||||
import kotlin.LazyThreadSafetyMode.NONE
|
import kotlin.LazyThreadSafetyMode.NONE
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
@@ -68,6 +75,8 @@ private const val OPEN_FILE_ACTION_CODE = 3
|
|||||||
private const val CHECK_NOTIFICATION_PERMISSION_ACTION_CODE = 4
|
private const val CHECK_NOTIFICATION_PERMISSION_ACTION_CODE = 4
|
||||||
|
|
||||||
private const val PREFS_NOTIFICATION_PERMISSION_ASKED = "NOTIFICATION_PERMISSION_ASKED"
|
private const val PREFS_NOTIFICATION_PERMISSION_ASKED = "NOTIFICATION_PERMISSION_ASKED"
|
||||||
|
private const val OPEN_FILE_AFTER_RESUME_DELAY_MS = 400L
|
||||||
|
private const val KEY_PENDING_OPEN_FILE_URI = "pending_open_file_uri"
|
||||||
|
|
||||||
class AmneziaActivity : QtActivity() {
|
class AmneziaActivity : QtActivity() {
|
||||||
|
|
||||||
@@ -84,6 +93,12 @@ class AmneziaActivity : QtActivity() {
|
|||||||
private val actionResultHandlers = mutableMapOf<Int, ActivityResultHandler>()
|
private val actionResultHandlers = mutableMapOf<Int, ActivityResultHandler>()
|
||||||
private val permissionRequestHandlers = mutableMapOf<Int, PermissionRequestHandler>()
|
private val permissionRequestHandlers = mutableMapOf<Int, PermissionRequestHandler>()
|
||||||
|
|
||||||
|
private var isActivityResumed = false
|
||||||
|
private var hasWindowFocus = false
|
||||||
|
private val resumeHandler = Handler(Looper.getMainLooper())
|
||||||
|
private var pendingOpenFileUri: String? = null
|
||||||
|
private var openFileDeliveryScheduled = false
|
||||||
|
|
||||||
private val vpnServiceEventHandler: Handler by lazy(NONE) {
|
private val vpnServiceEventHandler: Handler by lazy(NONE) {
|
||||||
object : Handler(Looper.getMainLooper()) {
|
object : Handler(Looper.getMainLooper()) {
|
||||||
override fun handleMessage(msg: Message) {
|
override fun handleMessage(msg: Message) {
|
||||||
@@ -170,10 +185,9 @@ class AmneziaActivity : QtActivity() {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
Log.d(TAG, "Create Amnezia activity")
|
Log.d(TAG, "Create Amnezia activity")
|
||||||
loadLibs()
|
loadLibs()
|
||||||
window.apply {
|
|
||||||
addFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
|
// Configure window for edge-to-edge display
|
||||||
statusBarColor = getColor(R.color.black)
|
configureWindowForEdgeToEdge()
|
||||||
}
|
|
||||||
mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||||
val proto = mainScope.async(Dispatchers.IO) {
|
val proto = mainScope.async(Dispatchers.IO) {
|
||||||
VpnStateStore.getVpnState().vpnProto
|
VpnStateStore.getVpnState().vpnProto
|
||||||
@@ -186,11 +200,18 @@ class AmneziaActivity : QtActivity() {
|
|||||||
doBindService()
|
doBindService()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
pendingOpenFileUri = savedInstanceState?.getString(KEY_PENDING_OPEN_FILE_URI)
|
||||||
|
openFileDeliveryScheduled = false
|
||||||
registerBroadcastReceivers()
|
registerBroadcastReceivers()
|
||||||
intent?.let(::processIntent)
|
intent?.let(::processIntent)
|
||||||
runBlocking { vpnProto = proto.await() }
|
runBlocking { vpnProto = proto.await() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
pendingOpenFileUri?.let { outState.putString(KEY_PENDING_OPEN_FILE_URI, it) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun loadLibs() {
|
private fun loadLibs() {
|
||||||
listOf(
|
listOf(
|
||||||
"rsapss",
|
"rsapss",
|
||||||
@@ -256,6 +277,11 @@ class AmneziaActivity : QtActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
|
isActivityResumed = false
|
||||||
|
hasWindowFocus = false
|
||||||
|
// Cancel all pending operations when activity stops
|
||||||
|
resumeHandler.removeCallbacksAndMessages(null)
|
||||||
|
openFileDeliveryScheduled = false
|
||||||
Log.d(TAG, "Stop Amnezia activity")
|
Log.d(TAG, "Stop Amnezia activity")
|
||||||
doUnbindService()
|
doUnbindService()
|
||||||
mainScope.launch {
|
mainScope.launch {
|
||||||
@@ -265,7 +291,197 @@ class AmneziaActivity : QtActivity() {
|
|||||||
super.onStop()
|
super.onStop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||||
|
super.onWindowFocusChanged(hasFocus)
|
||||||
|
hasWindowFocus = hasFocus
|
||||||
|
Log.d(TAG, "Window focus changed: hasFocus=$hasFocus")
|
||||||
|
|
||||||
|
if (!hasFocus) {
|
||||||
|
// Cancel pending operations if window loses focus
|
||||||
|
resumeHandler.removeCallbacksAndMessages(null)
|
||||||
|
} else if (isActivityResumed && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
window.decorView.apply {
|
||||||
|
invalidate()
|
||||||
|
resumeHandler.postDelayed({
|
||||||
|
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
|
||||||
|
sendTouch(1f, 1f)
|
||||||
|
}
|
||||||
|
}, 50)
|
||||||
|
resumeHandler.postDelayed({
|
||||||
|
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
|
||||||
|
sendTouch(2f, 2f)
|
||||||
|
requestLayout()
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
}, 150)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||||
|
val keyCode = event.keyCode
|
||||||
|
val pressed = event.action == KeyEvent.ACTION_DOWN
|
||||||
|
|
||||||
|
when (keyCode) {
|
||||||
|
KeyEvent.KEYCODE_BUTTON_A,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_B,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_X,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_Y,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_START,
|
||||||
|
KeyEvent.KEYCODE_BUTTON_SELECT -> {
|
||||||
|
nativeGamepadKeyEvent(0, keyCode, pressed)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_DPAD_CENTER,
|
||||||
|
KeyEvent.KEYCODE_DPAD_UP,
|
||||||
|
KeyEvent.KEYCODE_DPAD_DOWN,
|
||||||
|
KeyEvent.KEYCODE_DPAD_LEFT,
|
||||||
|
KeyEvent.KEYCODE_DPAD_RIGHT -> {
|
||||||
|
val syntheticKeyCode = if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) KeyEvent.KEYCODE_ENTER else keyCode
|
||||||
|
val synthetic = KeyEvent(
|
||||||
|
event.downTime, event.eventTime, event.action, syntheticKeyCode,
|
||||||
|
event.repeatCount, event.metaState, -1, event.scanCode,
|
||||||
|
event.flags, InputDevice.SOURCE_KEYBOARD
|
||||||
|
)
|
||||||
|
return super.dispatchKeyEvent(synthetic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.dispatchKeyEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
private external fun nativeGamepadKeyEvent(deviceId: Int, keyCode: Int, pressed: Boolean)
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
// Notify Qt to stop rendering BEFORE super.onPause() destroys the EGL surface.
|
||||||
|
// Using a coroutine here would be too late — the surface is gone by the time
|
||||||
|
// the coroutine runs. A direct synchronous call gives Qt's render thread the
|
||||||
|
// best chance to process visible=false before surface destruction.
|
||||||
|
if (qtInitialized.isCompleted) {
|
||||||
|
QtAndroidController.onActivityPaused()
|
||||||
|
}
|
||||||
|
super.onPause()
|
||||||
|
isActivityResumed = false
|
||||||
|
// Cancel all pending operations when activity pauses
|
||||||
|
resumeHandler.removeCallbacksAndMessages(null)
|
||||||
|
openFileDeliveryScheduled = false
|
||||||
|
Log.d(TAG, "Pause Amnezia activity")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
isActivityResumed = true
|
||||||
|
Log.d(TAG, "Resume Amnezia activity")
|
||||||
|
if (qtInitialized.isCompleted) {
|
||||||
|
QtAndroidController.onActivityResumed()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingOpenFileUri != null && !openFileDeliveryScheduled) {
|
||||||
|
val uri = pendingOpenFileUri!!
|
||||||
|
openFileDeliveryScheduled = true
|
||||||
|
resumeHandler.postDelayed({
|
||||||
|
if (!isFinishing && !isDestroyed) {
|
||||||
|
pendingOpenFileUri = null
|
||||||
|
openFileDeliveryScheduled = false
|
||||||
|
mainScope.launch {
|
||||||
|
qtInitialized.await()
|
||||||
|
QtAndroidController.onFileOpened(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, OPEN_FILE_AFTER_RESUME_DELAY_MS)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
window.decorView.apply {
|
||||||
|
invalidate()
|
||||||
|
|
||||||
|
resumeHandler.postDelayed({
|
||||||
|
// Check if activity is still resumed and has focus before executing
|
||||||
|
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
|
||||||
|
sendTouch(1f, 1f)
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
|
||||||
|
resumeHandler.postDelayed({
|
||||||
|
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
|
||||||
|
sendTouch(2f, 2f)
|
||||||
|
}
|
||||||
|
}, 200)
|
||||||
|
|
||||||
|
resumeHandler.postDelayed({
|
||||||
|
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
|
||||||
|
requestLayout()
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
}, 250)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
override fun onDestroy() {
|
||||||
|
isActivityResumed = false
|
||||||
|
hasWindowFocus = false
|
||||||
|
// Cancel all pending operations when activity is destroyed
|
||||||
|
resumeHandler.removeCallbacksAndMessages(null)
|
||||||
Log.d(TAG, "Destroy Amnezia activity")
|
Log.d(TAG, "Destroy Amnezia activity")
|
||||||
unregisterBroadcastReceiver(notificationStateReceiver)
|
unregisterBroadcastReceiver(notificationStateReceiver)
|
||||||
notificationStateReceiver = null
|
notificationStateReceiver = null
|
||||||
@@ -591,9 +807,13 @@ class AmneziaActivity : QtActivity() {
|
|||||||
grantUriPermission(packageName, this, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
grantUriPermission(packageName, this, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
}?.toString() ?: ""
|
}?.toString() ?: ""
|
||||||
Log.v(TAG, "Open file: $uri")
|
Log.v(TAG, "Open file: $uri")
|
||||||
mainScope.launch {
|
if (uri.isNotEmpty()) {
|
||||||
qtInitialized.await()
|
pendingOpenFileUri = uri
|
||||||
QtAndroidController.onFileOpened(uri)
|
} else {
|
||||||
|
mainScope.launch {
|
||||||
|
qtInitialized.await()
|
||||||
|
QtAndroidController.onFileOpened(uri)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
@@ -622,7 +842,7 @@ class AmneziaActivity : QtActivity() {
|
|||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun getFd(fileName: String): Int {
|
fun getFd(fileName: String): Int {
|
||||||
Log.v(TAG, "Get fd for $fileName")
|
Log.v(TAG, "Get fd for $fileName")
|
||||||
return blockingCall {
|
return blockingCall(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
pfd = contentResolver.openFileDescriptor(Uri.parse(fileName), "r")
|
pfd = contentResolver.openFileDescriptor(Uri.parse(fileName), "r")
|
||||||
pfd?.fd ?: -1
|
pfd?.fd ?: -1
|
||||||
@@ -666,6 +886,43 @@ class AmneziaActivity : QtActivity() {
|
|||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun isOnTv(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
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")
|
@Suppress("unused")
|
||||||
fun startQrCodeReader() {
|
fun startQrCodeReader() {
|
||||||
Log.v(TAG, "Start camera")
|
Log.v(TAG, "Start camera")
|
||||||
|
|||||||
@@ -565,7 +565,7 @@ open class AmneziaVpnService : VpnService() {
|
|||||||
protocolState.value = RECONNECTING
|
protocolState.value = RECONNECTING
|
||||||
|
|
||||||
connectionJob = connectionScope.launch {
|
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 name: String?
|
||||||
val packageName: String = pi.packageName
|
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
|
val isLaunchable: Boolean = pm.getLaunchIntentForPackage(packageName) != null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val name = ai.loadLabel(pm).toString()
|
val name = ai?.loadLabel(pm)?.toString()
|
||||||
this.name = if (name != packageName) name else null
|
this.name = name?.takeIf { it != packageName }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun compareTo(other: App): Int {
|
override fun compareTo(other: App): Int {
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package org.amnezia.vpn
|
package org.amnezia.vpn
|
||||||
|
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
@@ -11,8 +14,29 @@ private const val TAG = "TvFilePicker"
|
|||||||
|
|
||||||
class TvFilePicker : ComponentActivity() {
|
class TvFilePicker : ComponentActivity() {
|
||||||
|
|
||||||
private val fileChooseResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) {
|
private val fileChooseResultLauncher = registerForActivityResult(object : ActivityResultContracts.OpenDocument() {
|
||||||
setResult(RESULT_OK, Intent().apply { data = it })
|
override fun createIntent(context: Context, input: Array<String>): Intent {
|
||||||
|
val intent = super.createIntent(context, input)
|
||||||
|
|
||||||
|
val activitiesToResolveIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
context.packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()))
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
|
||||||
|
}
|
||||||
|
if (activitiesToResolveIntent.all {
|
||||||
|
val name = it.activityInfo.packageName
|
||||||
|
name.startsWith("com.google.android.tv.frameworkpackagestubs") || name.startsWith("com.android.tv.frameworkpackagestubs")
|
||||||
|
}) {
|
||||||
|
throw ActivityNotFoundException()
|
||||||
|
}
|
||||||
|
return intent
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
setResult(RESULT_OK, Intent().apply {
|
||||||
|
data = it
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
})
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +55,7 @@ class TvFilePicker : ComponentActivity() {
|
|||||||
private fun getFile() {
|
private fun getFile() {
|
||||||
try {
|
try {
|
||||||
Log.v(TAG, "getFile")
|
Log.v(TAG, "getFile")
|
||||||
fileChooseResultLauncher.launch("*/*")
|
fileChooseResultLauncher.launch(arrayOf("*/*"))
|
||||||
} catch (_: ActivityNotFoundException) {
|
} catch (_: ActivityNotFoundException) {
|
||||||
Log.w(TAG, "Activity not found")
|
Log.w(TAG, "Activity not found")
|
||||||
setResult(RESULT_CANCELED, Intent().apply { putExtra("activityNotFound", true) })
|
setResult(RESULT_CANCELED, Intent().apply { putExtra("activityNotFound", true) })
|
||||||
|
|||||||
@@ -28,4 +28,10 @@ object QtAndroidController {
|
|||||||
external fun onAuthResult(result: Boolean)
|
external fun onAuthResult(result: Boolean)
|
||||||
|
|
||||||
external fun decodeQrCode(data: String): Boolean
|
external fun decodeQrCode(data: String): Boolean
|
||||||
|
|
||||||
|
external fun onImeInsetsChanged(heightDp: Int)
|
||||||
|
external fun onSystemBarsInsetsChanged(navBarHeightDp: Int, statusBarHeightDp: Int)
|
||||||
|
|
||||||
|
external fun onActivityPaused()
|
||||||
|
external fun onActivityResumed()
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,7 @@ import org.amnezia.vpn.protocol.Protocol
|
|||||||
import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
|
import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
|
||||||
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
|
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
|
||||||
import org.amnezia.vpn.protocol.Statistics
|
import org.amnezia.vpn.protocol.Statistics
|
||||||
|
import org.amnezia.vpn.protocol.VpnException
|
||||||
import org.amnezia.vpn.protocol.VpnStartException
|
import org.amnezia.vpn.protocol.VpnStartException
|
||||||
import org.amnezia.vpn.util.LibraryLoader.loadSharedLibrary
|
import org.amnezia.vpn.util.LibraryLoader.loadSharedLibrary
|
||||||
import org.amnezia.vpn.util.Log
|
import org.amnezia.vpn.util.Log
|
||||||
@@ -27,6 +28,7 @@ private const val TAG = "Wireguard"
|
|||||||
open class Wireguard : Protocol() {
|
open class Wireguard : Protocol() {
|
||||||
|
|
||||||
private var tunnelHandle: Int = -1
|
private var tunnelHandle: Int = -1
|
||||||
|
private var config: WireguardConfig? = null // save config for reconnect
|
||||||
protected open val ifName: String = "amn0"
|
protected open val ifName: String = "amn0"
|
||||||
private lateinit var scope: CoroutineScope
|
private lateinit var scope: CoroutineScope
|
||||||
private var statusJob: Job? = null
|
private var statusJob: Job? = null
|
||||||
@@ -61,6 +63,7 @@ open class Wireguard : Protocol() {
|
|||||||
override suspend fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) {
|
override suspend fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) {
|
||||||
val wireguardConfig = parseConfig(config)
|
val wireguardConfig = parseConfig(config)
|
||||||
start(wireguardConfig, vpnBuilder, protect)
|
start(wireguardConfig, vpnBuilder, protect)
|
||||||
|
this.config = wireguardConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun parseConfig(config: JSONObject): 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("S2")?.let { setS2(it.toInt()) }
|
||||||
configData.optStringOrNull("S3")?.let { setS3(it.toInt()) }
|
configData.optStringOrNull("S3")?.let { setS3(it.toInt()) }
|
||||||
configData.optStringOrNull("S4")?.let { setS4(it.toInt()) }
|
configData.optStringOrNull("S4")?.let { setS4(it.toInt()) }
|
||||||
configData.optStringOrNull("H1")?.let { setH1(it.toLong()) }
|
configData.optStringOrNull("H1")?.trim()?.let { if (it.isNotEmpty()) setH1(it) }
|
||||||
configData.optStringOrNull("H2")?.let { setH2(it.toLong()) }
|
configData.optStringOrNull("H2")?.trim()?.let { if (it.isNotEmpty()) setH2(it) }
|
||||||
configData.optStringOrNull("H3")?.let { setH3(it.toLong()) }
|
configData.optStringOrNull("H3")?.trim()?.let { if (it.isNotEmpty()) setH3(it) }
|
||||||
configData.optStringOrNull("H4")?.let { setH4(it.toLong()) }
|
configData.optStringOrNull("H4")?.trim()?.let { if (it.isNotEmpty()) setH4(it) }
|
||||||
configData.optStringOrNull("I1")?.let { setI1(it) }
|
configData.optStringOrNull("I1")?.let { setI1(it) }
|
||||||
configData.optStringOrNull("I2")?.let { setI2(it) }
|
configData.optStringOrNull("I2")?.let { setI2(it) }
|
||||||
configData.optStringOrNull("I3")?.let { setI3(it) }
|
configData.optStringOrNull("I3")?.let { setI3(it) }
|
||||||
configData.optStringOrNull("I4")?.let { setI4(it) }
|
configData.optStringOrNull("I4")?.let { setI4(it) }
|
||||||
configData.optStringOrNull("I5")?.let { setI5(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) {
|
private fun start(
|
||||||
if (tunnelHandle != -1) {
|
config: WireguardConfig,
|
||||||
|
vpnBuilder: Builder,
|
||||||
|
protect: (Int) -> Boolean,
|
||||||
|
stopExistingVpn: Boolean = false
|
||||||
|
) {
|
||||||
|
if (!stopExistingVpn && tunnelHandle != -1) {
|
||||||
Log.w(TAG, "Tunnel already up")
|
Log.w(TAG, "Tunnel already up")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -146,6 +150,9 @@ open class Wireguard : Protocol() {
|
|||||||
buildVpnInterface(config, vpnBuilder)
|
buildVpnInterface(config, vpnBuilder)
|
||||||
|
|
||||||
vpnBuilder.establish().use { tunFd ->
|
vpnBuilder.establish().use { tunFd ->
|
||||||
|
if (stopExistingVpn && tunnelHandle != -1) {
|
||||||
|
turnOffVpn()
|
||||||
|
}
|
||||||
if (tunFd == null) {
|
if (tunFd == null) {
|
||||||
throw VpnStartException("Create VPN interface: permission not granted or revoked")
|
throw VpnStartException("Create VPN interface: permission not granted or revoked")
|
||||||
}
|
}
|
||||||
@@ -202,20 +209,25 @@ open class Wireguard : Protocol() {
|
|||||||
return lastHandshake
|
return lastHandshake
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stopVpn() {
|
private fun turnOffVpn() {
|
||||||
if (tunnelHandle == -1) {
|
|
||||||
Log.w(TAG, "Tunnel already down")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
statusJob?.cancel()
|
statusJob?.cancel()
|
||||||
statusJob = null
|
statusJob = null
|
||||||
val handleToClose = tunnelHandle
|
val handleToClose = tunnelHandle
|
||||||
tunnelHandle = -1
|
tunnelHandle = -1
|
||||||
GoBackend.awgTurnOff(handleToClose)
|
GoBackend.awgTurnOff(handleToClose)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stopVpn() {
|
||||||
|
if (tunnelHandle == -1) {
|
||||||
|
Log.w(TAG, "Tunnel already down")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
turnOffVpn()
|
||||||
state.value = DISCONNECTED
|
state.value = DISCONNECTED
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun reconnectVpn(vpnBuilder: Builder) {
|
override fun reconnectVpn(vpnBuilder: Builder, protect: (Int) -> Boolean) {
|
||||||
state.value = CONNECTED
|
val config = this.config ?: throw VpnException("Reconnect config is empty")
|
||||||
|
start(config, vpnBuilder, protect, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,19 +22,15 @@ open class WireguardConfig protected constructor(
|
|||||||
val s2: Int?,
|
val s2: Int?,
|
||||||
val s3: Int?,
|
val s3: Int?,
|
||||||
val s4: Int?,
|
val s4: Int?,
|
||||||
val h1: Long?,
|
val h1: String?,
|
||||||
val h2: Long?,
|
val h2: String?,
|
||||||
val h3: Long?,
|
val h3: String?,
|
||||||
val h4: Long?,
|
val h4: String?,
|
||||||
var i1: String?,
|
var i1: String?,
|
||||||
var i2: String?,
|
var i2: String?,
|
||||||
var i3: String?,
|
var i3: String?,
|
||||||
var i4: String?,
|
var i4: String?,
|
||||||
var i5: String?,
|
var i5: String?,
|
||||||
var j1: String?,
|
|
||||||
var j2: String?,
|
|
||||||
var j3: String?,
|
|
||||||
var itime: Int?
|
|
||||||
) : ProtocolConfig(protocolConfigBuilder) {
|
) : ProtocolConfig(protocolConfigBuilder) {
|
||||||
|
|
||||||
protected constructor(builder: Builder) : this(
|
protected constructor(builder: Builder) : this(
|
||||||
@@ -61,10 +57,6 @@ open class WireguardConfig protected constructor(
|
|||||||
builder.i3,
|
builder.i3,
|
||||||
builder.i4,
|
builder.i4,
|
||||||
builder.i5,
|
builder.i5,
|
||||||
builder.j1,
|
|
||||||
builder.j2,
|
|
||||||
builder.j3,
|
|
||||||
builder.itime
|
|
||||||
)
|
)
|
||||||
|
|
||||||
fun toWgUserspaceString(): String = with(StringBuilder()) {
|
fun toWgUserspaceString(): String = with(StringBuilder()) {
|
||||||
@@ -94,10 +86,6 @@ open class WireguardConfig protected constructor(
|
|||||||
i3?.let { appendLine("i3=$it") }
|
i3?.let { appendLine("i3=$it") }
|
||||||
i4?.let { appendLine("i4=$it") }
|
i4?.let { appendLine("i4=$it") }
|
||||||
i5?.let { appendLine("i5=$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 s2: Int? = null
|
||||||
internal var s3: Int? = null
|
internal var s3: Int? = null
|
||||||
internal var s4: Int? = null
|
internal var s4: Int? = null
|
||||||
internal var h1: Long? = null
|
internal var h1: String? = null
|
||||||
internal var h2: Long? = null
|
internal var h2: String? = null
|
||||||
internal var h3: Long? = null
|
internal var h3: String? = null
|
||||||
internal var h4: Long? = null
|
internal var h4: String? = null
|
||||||
internal var i1: String? = null
|
internal var i1: String? = null
|
||||||
internal var i2: String? = null
|
internal var i2: String? = null
|
||||||
internal var i3: String? = null
|
internal var i3: String? = null
|
||||||
internal var i4: String? = null
|
internal var i4: String? = null
|
||||||
internal var i5: 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 }
|
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 setS2(s2: Int) = apply { this.s2 = s2 }
|
||||||
fun setS3(s3: Int) = apply { this.s3 = s3 }
|
fun setS3(s3: Int) = apply { this.s3 = s3 }
|
||||||
fun setS4(s4: Int) = apply { this.s4 = s4 }
|
fun setS4(s4: Int) = apply { this.s4 = s4 }
|
||||||
fun setH1(h1: Long) = apply { this.h1 = h1 }
|
fun setH1(h1: String) = apply { this.h1 = h1 }
|
||||||
fun setH2(h2: Long) = apply { this.h2 = h2 }
|
fun setH2(h2: String) = apply { this.h2 = h2 }
|
||||||
fun setH3(h3: Long) = apply { this.h3 = h3 }
|
fun setH3(h3: String) = apply { this.h3 = h3 }
|
||||||
fun setH4(h4: Long) = apply { this.h4 = h4 }
|
fun setH4(h4: String) = apply { this.h4 = h4 }
|
||||||
fun setI1(i1: String) = apply { this.i1 = i1 }
|
fun setI1(i1: String) = apply { this.i1 = i1 }
|
||||||
fun setI2(i2: String) = apply { this.i2 = i2 }
|
fun setI2(i2: String) = apply { this.i2 = i2 }
|
||||||
fun setI3(i3: String) = apply { this.i3 = i3 }
|
fun setI3(i3: String) = apply { this.i3 = i3 }
|
||||||
fun setI4(i4: String) = apply { this.i4 = i4 }
|
fun setI4(i4: String) = apply { this.i4 = i4 }
|
||||||
fun setI5(i5: String) = apply { this.i5 = i5 }
|
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) }
|
override fun build(): WireguardConfig = configBuild().run { WireguardConfig(this@Builder) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import android.content.Context
|
|||||||
import android.net.VpnService.Builder
|
import android.net.VpnService.Builder
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.net.ServerSocket
|
||||||
|
import java.util.UUID
|
||||||
import go.Seq
|
import go.Seq
|
||||||
import org.amnezia.vpn.protocol.BadConfigException
|
import org.amnezia.vpn.protocol.BadConfigException
|
||||||
import org.amnezia.vpn.protocol.Protocol
|
import org.amnezia.vpn.protocol.Protocol
|
||||||
@@ -19,11 +22,32 @@ import org.amnezia.vpn.util.Log
|
|||||||
import org.amnezia.vpn.util.net.InetNetwork
|
import org.amnezia.vpn.util.net.InetNetwork
|
||||||
import org.amnezia.vpn.util.net.ip
|
import org.amnezia.vpn.util.net.ip
|
||||||
import org.amnezia.vpn.util.net.parseInetAddress
|
import org.amnezia.vpn.util.net.parseInetAddress
|
||||||
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
||||||
private const val TAG = "Xray"
|
private const val TAG = "Xray"
|
||||||
private const val LIBXRAY_TAG = "libXray"
|
private const val LIBXRAY_TAG = "libXray"
|
||||||
|
|
||||||
|
private fun findSocksInboundIndex(inbounds: JSONArray): Int {
|
||||||
|
for (i in 0 until inbounds.length()) {
|
||||||
|
val o = inbounds.optJSONObject(i) ?: continue
|
||||||
|
if (o.optString("protocol").equals("socks", ignoreCase = true)) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun acquireFreeLocalPort(): Int {
|
||||||
|
try {
|
||||||
|
ServerSocket(0, 1, InetAddress.getByName("127.0.0.1")).use { return it.localPort }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw VpnStartException(
|
||||||
|
"Failed to acquire free TCP port on 127.0.0.1 for SOCKS inbound: ${e.message}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class Xray : Protocol() {
|
class Xray : Protocol() {
|
||||||
|
|
||||||
private var isRunning: Boolean = false
|
private var isRunning: Boolean = false
|
||||||
@@ -56,6 +80,10 @@ class Xray : Protocol() {
|
|||||||
val xrayJsonConfig = config.optJSONObject("xray_config_data")
|
val xrayJsonConfig = config.optJSONObject("xray_config_data")
|
||||||
?: config.optJSONObject("ssxray_config_data")
|
?: config.optJSONObject("ssxray_config_data")
|
||||||
?: throw BadConfigException("config_data not found")
|
?: throw BadConfigException("config_data not found")
|
||||||
|
|
||||||
|
// Inject SOCKS5 auth before starting xray. Re-uses existing credentials if present.
|
||||||
|
ensureInboundAuth(xrayJsonConfig)
|
||||||
|
|
||||||
val xrayConfig = parseConfig(config, xrayJsonConfig)
|
val xrayConfig = parseConfig(config, xrayJsonConfig)
|
||||||
|
|
||||||
(xrayJsonConfig.optJSONObject("log") ?: JSONObject().also { xrayJsonConfig.put("log", it) })
|
(xrayJsonConfig.optJSONObject("log") ?: JSONObject().also { xrayJsonConfig.put("log", it) })
|
||||||
@@ -97,9 +125,22 @@ class Xray : Protocol() {
|
|||||||
if (it.isNotBlank()) setMtu(it.toInt())
|
if (it.isNotBlank()) setMtu(it.toInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
val socksConfig = xrayJsonConfig.getJSONArray("inbounds")[0] as JSONObject
|
val inbounds = xrayJsonConfig.getJSONArray("inbounds")
|
||||||
|
val socksIdx = findSocksInboundIndex(inbounds)
|
||||||
|
if (socksIdx < 0) {
|
||||||
|
throw BadConfigException("socks inbound not found")
|
||||||
|
}
|
||||||
|
val socksConfig = inbounds.getJSONObject(socksIdx)
|
||||||
socksConfig.getInt("port").let { setSocksPort(it) }
|
socksConfig.getInt("port").let { setSocksPort(it) }
|
||||||
|
|
||||||
|
val socksSettings = socksConfig.optJSONObject("settings")
|
||||||
|
val accounts = socksSettings?.optJSONArray("accounts")
|
||||||
|
if (accounts != null && accounts.length() > 0) {
|
||||||
|
val account = accounts.getJSONObject(0)
|
||||||
|
setSocksUser(account.optString("user"))
|
||||||
|
setSocksPass(account.optString("pass"))
|
||||||
|
}
|
||||||
|
|
||||||
configSplitTunneling(config)
|
configSplitTunneling(config)
|
||||||
configAppSplitTunneling(config)
|
configAppSplitTunneling(config)
|
||||||
}
|
}
|
||||||
@@ -157,22 +198,54 @@ class Xray : Protocol() {
|
|||||||
state.value = DISCONNECTED
|
state.value = DISCONNECTED
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun reconnectVpn(vpnBuilder: Builder) {
|
override fun reconnectVpn(vpnBuilder: Builder, protect: (Int) -> Boolean) {
|
||||||
state.value = CONNECTED
|
state.value = CONNECTED
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun runTun2Socks(config: XrayConfig, fd: Int) {
|
private fun runTun2Socks(config: XrayConfig, fd: Int) {
|
||||||
|
val proxyUrl = "socks5://${config.socksUser}:${config.socksPass}@127.0.0.1:${config.socksPort}"
|
||||||
val tun2SocksConfig = Tun2SocksConfig().apply {
|
val tun2SocksConfig = Tun2SocksConfig().apply {
|
||||||
mtu = config.mtu.toLong()
|
mtu = config.mtu.toLong()
|
||||||
proxy = "socks5://127.0.0.1:${config.socksPort}"
|
proxy = proxyUrl
|
||||||
device = "fd://$fd"
|
device = "fd://$fd"
|
||||||
logLevel = "warning"
|
logLevel = "warn"
|
||||||
}
|
}
|
||||||
LibXray.startTun2Socks(tun2SocksConfig, fd.toLong()).isNotNullOrBlank { err ->
|
LibXray.startTun2Socks(tun2SocksConfig, fd.toLong()).isNotNullOrBlank { err ->
|
||||||
throw VpnStartException("Failed to start tun2socks: $err")
|
throw VpnStartException("Failed to start tun2socks: $err")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensures SOCKS5 auth is present on the socks inbound settings.
|
||||||
|
// Re-uses existing credentials if already configured; otherwise generates random ones.
|
||||||
|
private fun ensureInboundAuth(xrayConfig: JSONObject) {
|
||||||
|
val inbounds = xrayConfig.optJSONArray("inbounds") ?: return
|
||||||
|
val socksIdx = findSocksInboundIndex(inbounds)
|
||||||
|
if (socksIdx < 0) return
|
||||||
|
|
||||||
|
val inbound = inbounds.getJSONObject(socksIdx)
|
||||||
|
inbound.put("port", acquireFreeLocalPort())
|
||||||
|
val settings = inbound.optJSONObject("settings") ?: JSONObject().also { inbound.put("settings", it) }
|
||||||
|
val accounts = settings.optJSONArray("accounts")
|
||||||
|
if (accounts != null && accounts.length() > 0) {
|
||||||
|
val account = accounts.getJSONObject(0)
|
||||||
|
if (account.optString("user").isNotEmpty() && account.optString("pass").isNotEmpty()) {
|
||||||
|
// Ensure auth mode is enforced even for imported configs that had accounts
|
||||||
|
// but auth: "noauth" (or no auth field).
|
||||||
|
settings.put("auth", "password")
|
||||||
|
inbound.put("settings", settings)
|
||||||
|
inbounds.put(socksIdx, inbound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val user = UUID.randomUUID().toString().replace("-", "").substring(0, 16)
|
||||||
|
val pass = UUID.randomUUID().toString().replace("-", "")
|
||||||
|
settings.put("auth", "password")
|
||||||
|
settings.put("accounts", JSONArray().put(JSONObject().put("user", user).put("pass", pass)))
|
||||||
|
inbound.put("settings", settings)
|
||||||
|
inbounds.put(socksIdx, inbound)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val instance: Xray by lazy { Xray() }
|
val instance: Xray by lazy { Xray() }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,16 @@ private const val XRAY_DEFAULT_MAX_MEMORY: Long = 50 shl 20 // 50 MB
|
|||||||
class XrayConfig protected constructor(
|
class XrayConfig protected constructor(
|
||||||
protocolConfigBuilder: ProtocolConfig.Builder,
|
protocolConfigBuilder: ProtocolConfig.Builder,
|
||||||
val socksPort: Int,
|
val socksPort: Int,
|
||||||
|
val socksUser: String,
|
||||||
|
val socksPass: String,
|
||||||
val maxMemory: Long,
|
val maxMemory: Long,
|
||||||
) : ProtocolConfig(protocolConfigBuilder) {
|
) : ProtocolConfig(protocolConfigBuilder) {
|
||||||
|
|
||||||
protected constructor(builder: Builder) : this(
|
protected constructor(builder: Builder) : this(
|
||||||
builder,
|
builder,
|
||||||
builder.socksPort,
|
builder.socksPort,
|
||||||
|
builder.socksUser,
|
||||||
|
builder.socksPass,
|
||||||
builder.maxMemory
|
builder.maxMemory
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -22,6 +26,12 @@ class XrayConfig protected constructor(
|
|||||||
internal var socksPort: Int = 0
|
internal var socksPort: Int = 0
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
internal var socksUser: String = ""
|
||||||
|
private set
|
||||||
|
|
||||||
|
internal var socksPass: String = ""
|
||||||
|
private set
|
||||||
|
|
||||||
internal var maxMemory: Long = XRAY_DEFAULT_MAX_MEMORY
|
internal var maxMemory: Long = XRAY_DEFAULT_MAX_MEMORY
|
||||||
private set
|
private set
|
||||||
|
|
||||||
@@ -29,6 +39,10 @@ class XrayConfig protected constructor(
|
|||||||
|
|
||||||
fun setSocksPort(port: Int) = apply { socksPort = port }
|
fun setSocksPort(port: Int) = apply { socksPort = port }
|
||||||
|
|
||||||
|
fun setSocksUser(user: String) = apply { socksUser = user }
|
||||||
|
|
||||||
|
fun setSocksPass(pass: String) = apply { socksPass = pass }
|
||||||
|
|
||||||
fun setMaxMemory(maxMemory: Long) = apply { this.maxMemory = maxMemory }
|
fun setMaxMemory(maxMemory: Long) = apply { this.maxMemory = maxMemory }
|
||||||
|
|
||||||
override fun build(): XrayConfig = configBuild().run { XrayConfig(this@Builder) }
|
override fun build(): XrayConfig = configBuild().run { XrayConfig(this@Builder) }
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ elseif(APPLE AND NOT IOS)
|
|||||||
endif()
|
endif()
|
||||||
set(OPENSSL_INCLUDE_DIR "${OPENSSL_ROOT_DIR}/macos/include")
|
set(OPENSSL_INCLUDE_DIR "${OPENSSL_ROOT_DIR}/macos/include")
|
||||||
set(OPENSSL_LIB_SSL_PATH "${OPENSSL_ROOT_DIR}/macos/lib/libssl.a")
|
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)
|
elseif(IOS)
|
||||||
set(LIBSSH_INCLUDE_DIR "${LIBSSH_ROOT_DIR}/ios/arm64")
|
set(LIBSSH_INCLUDE_DIR "${LIBSSH_ROOT_DIR}/ios/arm64")
|
||||||
set(LIBSSH_LIB_PATH "${LIBSSH_ROOT_DIR}/ios/arm64/libssh.a")
|
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_SSL_PATH "${OPENSSL_ROOT_DIR}/linux/x86_64/libssl.a")
|
||||||
set(OPENSSL_LIB_CRYPTO_PATH "${OPENSSL_ROOT_DIR}/linux/x86_64/libcrypto.a")
|
set(OPENSSL_LIB_CRYPTO_PATH "${OPENSSL_ROOT_DIR}/linux/x86_64/libcrypto.a")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
file(COPY ${OPENSSL_LIB_SSL_PATH} ${OPENSSL_LIB_CRYPTO_PATH}
|
file(COPY ${OPENSSL_LIB_SSL_PATH} ${OPENSSL_LIB_CRYPTO_PATH}
|
||||||
DESTINATION ${OPENSSL_LIBRARIES_DIR})
|
DESTINATION ${OPENSSL_LIBRARIES_DIR})
|
||||||
|
|
||||||
@@ -83,6 +83,26 @@ add_compile_definitions(_WINSOCKAPI_)
|
|||||||
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
|
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
|
||||||
set(BUILD_WITH_QT6 ON)
|
set(BUILD_WITH_QT6 ON)
|
||||||
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtkeychain)
|
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtkeychain)
|
||||||
|
|
||||||
|
if(ANDROID)
|
||||||
|
# Use qtgamepad from amnezia-vpn/qtgamepad repository
|
||||||
|
# Only if Qt6CorePrivate is available (required by qtgamepad)
|
||||||
|
find_package(Qt6CorePrivate CONFIG QUIET)
|
||||||
|
if(Qt6CorePrivate_FOUND)
|
||||||
|
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/qtgamepad)
|
||||||
|
# Link both the C++ module and QML plugin
|
||||||
|
if(TARGET GamepadLegacy)
|
||||||
|
target_link_libraries(${PROJECT} PRIVATE GamepadLegacy)
|
||||||
|
endif()
|
||||||
|
if(TARGET GamepadLegacyQuickPrivate)
|
||||||
|
target_link_libraries(${PROJECT} PRIVATE GamepadLegacyQuickPrivate)
|
||||||
|
endif()
|
||||||
|
message(STATUS "Gamepad support enabled for Android")
|
||||||
|
else()
|
||||||
|
message(STATUS "Qt6CorePrivate not found. Gamepad support disabled for Android.")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
set(LIBS ${LIBS} qt6keychain)
|
set(LIBS ${LIBS} qt6keychain)
|
||||||
|
|
||||||
include_directories(
|
include_directories(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
message("Client android ${CMAKE_ANDROID_ARCH_ABI} build")
|
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
|
set(ANDROID_PLATFORM "android-${APP_ANDROID_MIN_SDK}" CACHE STRING
|
||||||
"The minimum API level supported by the application or library" FORCE)
|
"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_NAME ${CMAKE_PROJECT_VERSION}
|
||||||
QT_ANDROID_VERSION_CODE ${APP_ANDROID_VERSION_CODE}
|
QT_ANDROID_VERSION_CODE ${APP_ANDROID_VERSION_CODE}
|
||||||
QT_ANDROID_MIN_SDK_VERSION ${APP_ANDROID_MIN_SDK}
|
QT_ANDROID_MIN_SDK_VERSION ${APP_ANDROID_MIN_SDK}
|
||||||
QT_ANDROID_TARGET_SDK_VERSION 34
|
QT_ANDROID_TARGET_SDK_VERSION 36
|
||||||
QT_ANDROID_SDK_BUILD_TOOLS_REVISION 34.0.0
|
QT_ANDROID_SDK_BUILD_TOOLS_REVISION 36.0.0
|
||||||
QT_ANDROID_PACKAGE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/android
|
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
|
# We need to include qtprivate api's
|
||||||
# As QAndroidBinder is not yet implemented with a public api
|
# 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)
|
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/ios_controller_wrapper.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.h
|
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.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
|
${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)
|
set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h PROPERTIES OBJECTIVE_CPP_HEADER TRUE)
|
||||||
@@ -46,6 +47,7 @@ set(SOURCES ${SOURCES}
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
|
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
|
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.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
|
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/AmneziaSceneDelegateHooks.mm
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -119,6 +121,7 @@ target_sources(${PROJECT} PRIVATE
|
|||||||
${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift
|
${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift
|
||||||
${CLIENT_ROOT_DIR}/platforms/ios/ScreenProtection.swift
|
${CLIENT_ROOT_DIR}/platforms/ios/ScreenProtection.swift
|
||||||
${CLIENT_ROOT_DIR}/platforms/ios/VPNCController.swift
|
${CLIENT_ROOT_DIR}/platforms/ios/VPNCController.swift
|
||||||
|
${CLIENT_ROOT_DIR}/platforms/ios/StoreKit2Helper.swift
|
||||||
)
|
)
|
||||||
|
|
||||||
target_sources(${PROJECT} PRIVATE
|
target_sources(${PROJECT} PRIVATE
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ set(HEADERS ${HEADERS}
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h
|
${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/ios_controller_wrapper.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.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.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate-C-Interface.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.mm
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.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/iosnotificationhandler.mm
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/StoreKitController.mm
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
|
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
|
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm
|
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm
|
||||||
@@ -129,6 +131,7 @@ target_sources(${PROJECT} PRIVATE
|
|||||||
${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift
|
${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift
|
||||||
${CLIENT_ROOT_DIR}/platforms/ios/ScreenProtection.swift
|
${CLIENT_ROOT_DIR}/platforms/ios/ScreenProtection.swift
|
||||||
${CLIENT_ROOT_DIR}/platforms/ios/VPNCController.swift
|
${CLIENT_ROOT_DIR}/platforms/ios/VPNCController.swift
|
||||||
|
${CLIENT_ROOT_DIR}/platforms/ios/StoreKit2Helper.swift
|
||||||
)
|
)
|
||||||
|
|
||||||
target_sources(${PROJECT} PRIVATE
|
target_sources(${PROJECT} PRIVATE
|
||||||
@@ -161,7 +164,7 @@ add_custom_command(TARGET ${PROJECT} POST_BUILD
|
|||||||
COMMAND ${CMAKE_COMMAND} -E make_directory
|
COMMAND ${CMAKE_COMMAND} -E make_directory
|
||||||
$<TARGET_BUNDLE_DIR:AmneziaVPN>/Contents/Frameworks
|
$<TARGET_BUNDLE_DIR:AmneziaVPN>/Contents/Frameworks
|
||||||
COMMAND /usr/bin/find "$<TARGET_BUNDLE_DIR:AmneziaVPN>/Contents/Frameworks/OpenVPNAdapter.framework" -name "*.sha256" -delete
|
COMMAND /usr/bin/find "$<TARGET_BUNDLE_DIR:AmneziaVPN>/Contents/Frameworks/OpenVPNAdapter.framework" -name "*.sha256" -delete
|
||||||
COMMAND /usr/bin/codesign --force --sign "Apple Distribution"
|
COMMAND /usr/bin/codesign --force --sign "Apple Distribution: Privacy Technologies OU"
|
||||||
"$<TARGET_BUNDLE_DIR:AmneziaVPN>/Contents/Frameworks/OpenVPNAdapter.framework/Versions/Current/OpenVPNAdapter"
|
"$<TARGET_BUNDLE_DIR:AmneziaVPN>/Contents/Frameworks/OpenVPNAdapter.framework/Versions/Current/OpenVPNAdapter"
|
||||||
COMMAND ${QT_BIN_DIR_DETECTED}/macdeployqt $<TARGET_BUNDLE_DIR:AmneziaVPN> -appstore-compliant -qmldir=${CMAKE_CURRENT_SOURCE_DIR}
|
COMMAND ${QT_BIN_DIR_DETECTED}/macdeployqt $<TARGET_BUNDLE_DIR:AmneziaVPN> -appstore-compliant -qmldir=${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
COMMENT "Signing OpenVPNAdapter framework"
|
COMMENT "Signing OpenVPNAdapter framework"
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ set(HEADERS ${HEADERS}
|
|||||||
${CLIENT_ROOT_DIR}/../common/logger/logger.h
|
${CLIENT_ROOT_DIR}/../common/logger/logger.h
|
||||||
${CLIENT_ROOT_DIR}/utils/qmlUtils.h
|
${CLIENT_ROOT_DIR}/utils/qmlUtils.h
|
||||||
${CLIENT_ROOT_DIR}/core/api/apiUtils.h
|
${CLIENT_ROOT_DIR}/core/api/apiUtils.h
|
||||||
|
${CLIENT_ROOT_DIR}/core/osSignalHandler.h
|
||||||
)
|
)
|
||||||
|
|
||||||
# Mozilla headres
|
# Mozilla headres
|
||||||
@@ -36,7 +37,6 @@ set(HEADERS ${HEADERS}
|
|||||||
${CLIENT_ROOT_DIR}/mozilla/shared/ipaddress.h
|
${CLIENT_ROOT_DIR}/mozilla/shared/ipaddress.h
|
||||||
${CLIENT_ROOT_DIR}/mozilla/shared/leakdetector.h
|
${CLIENT_ROOT_DIR}/mozilla/shared/leakdetector.h
|
||||||
${CLIENT_ROOT_DIR}/mozilla/controllerimpl.h
|
${CLIENT_ROOT_DIR}/mozilla/controllerimpl.h
|
||||||
${CLIENT_ROOT_DIR}/mozilla/localsocketcontroller.h
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if(NOT IOS AND NOT MACOS_NE)
|
if(NOT IOS AND NOT MACOS_NE)
|
||||||
@@ -79,6 +79,7 @@ set(SOURCES ${SOURCES}
|
|||||||
${CLIENT_ROOT_DIR}/../common/logger/logger.cpp
|
${CLIENT_ROOT_DIR}/../common/logger/logger.cpp
|
||||||
${CLIENT_ROOT_DIR}/utils/qmlUtils.cpp
|
${CLIENT_ROOT_DIR}/utils/qmlUtils.cpp
|
||||||
${CLIENT_ROOT_DIR}/core/api/apiUtils.cpp
|
${CLIENT_ROOT_DIR}/core/api/apiUtils.cpp
|
||||||
|
${CLIENT_ROOT_DIR}/core/osSignalHandler.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
# Mozilla sources
|
# Mozilla sources
|
||||||
@@ -86,7 +87,6 @@ set(SOURCES ${SOURCES}
|
|||||||
${CLIENT_ROOT_DIR}/mozilla/models/server.cpp
|
${CLIENT_ROOT_DIR}/mozilla/models/server.cpp
|
||||||
${CLIENT_ROOT_DIR}/mozilla/shared/ipaddress.cpp
|
${CLIENT_ROOT_DIR}/mozilla/shared/ipaddress.cpp
|
||||||
${CLIENT_ROOT_DIR}/mozilla/shared/leakdetector.cpp
|
${CLIENT_ROOT_DIR}/mozilla/shared/leakdetector.cpp
|
||||||
${CLIENT_ROOT_DIR}/mozilla/localsocketcontroller.cpp
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if(NOT IOS AND NOT MACOS_NE)
|
if(NOT IOS AND NOT MACOS_NE)
|
||||||
@@ -175,13 +175,12 @@ if(WIN32)
|
|||||||
)
|
)
|
||||||
endif()
|
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")
|
message("Client desktop build")
|
||||||
add_compile_definitions(AMNEZIA_DESKTOP)
|
add_compile_definitions(AMNEZIA_DESKTOP)
|
||||||
|
|
||||||
set(HEADERS ${HEADERS}
|
set(HEADERS ${HEADERS}
|
||||||
${CLIENT_ROOT_DIR}/core/ipcclient.h
|
${CLIENT_ROOT_DIR}/core/ipcclient.h
|
||||||
${CLIENT_ROOT_DIR}/core/privileged_process.h
|
|
||||||
${CLIENT_ROOT_DIR}/ui/systemtray_notificationhandler.h
|
${CLIENT_ROOT_DIR}/ui/systemtray_notificationhandler.h
|
||||||
${CLIENT_ROOT_DIR}/protocols/openvpnprotocol.h
|
${CLIENT_ROOT_DIR}/protocols/openvpnprotocol.h
|
||||||
${CLIENT_ROOT_DIR}/protocols/openvpnovercloakprotocol.h
|
${CLIENT_ROOT_DIR}/protocols/openvpnovercloakprotocol.h
|
||||||
@@ -189,11 +188,12 @@ if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
|
|||||||
${CLIENT_ROOT_DIR}/protocols/wireguardprotocol.h
|
${CLIENT_ROOT_DIR}/protocols/wireguardprotocol.h
|
||||||
${CLIENT_ROOT_DIR}/protocols/xrayprotocol.h
|
${CLIENT_ROOT_DIR}/protocols/xrayprotocol.h
|
||||||
${CLIENT_ROOT_DIR}/protocols/awgprotocol.h
|
${CLIENT_ROOT_DIR}/protocols/awgprotocol.h
|
||||||
|
${CLIENT_ROOT_DIR}/mozilla/localsocketcontroller.h
|
||||||
)
|
)
|
||||||
|
|
||||||
set(SOURCES ${SOURCES}
|
set(SOURCES ${SOURCES}
|
||||||
${CLIENT_ROOT_DIR}/core/ipcclient.cpp
|
${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}/ui/systemtray_notificationhandler.cpp
|
||||||
${CLIENT_ROOT_DIR}/protocols/openvpnprotocol.cpp
|
${CLIENT_ROOT_DIR}/protocols/openvpnprotocol.cpp
|
||||||
${CLIENT_ROOT_DIR}/protocols/openvpnovercloakprotocol.cpp
|
${CLIENT_ROOT_DIR}/protocols/openvpnovercloakprotocol.cpp
|
||||||
@@ -203,3 +203,14 @@ if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
|
|||||||
${CLIENT_ROOT_DIR}/protocols/awgprotocol.cpp
|
${CLIENT_ROOT_DIR}/protocols/awgprotocol.cpp
|
||||||
)
|
)
|
||||||
endif()
|
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::underloadPacketMagicHeader] = configMap.value(config_key::underloadPacketMagicHeader);
|
||||||
jsonConfig[config_key::transportPacketMagicHeader] = configMap.value(config_key::transportPacketMagicHeader);
|
jsonConfig[config_key::transportPacketMagicHeader] = configMap.value(config_key::transportPacketMagicHeader);
|
||||||
|
|
||||||
// jsonConfig[config_key::cookieReplyPacketJunkSize] = configMap.value(config_key::cookieReplyPacketJunkSize);
|
if (container == DockerContainer::Awg2) {
|
||||||
// jsonConfig[config_key::transportPacketJunkSize] = configMap.value(config_key::transportPacketJunkSize);
|
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::specialJunk1] = configMap.value(amnezia::config_key::specialJunk1);
|
||||||
// jsonConfig[config_key::specialJunk2] = configMap.value(amnezia::config_key::specialJunk2);
|
jsonConfig[config_key::specialJunk2] = configMap.value(amnezia::config_key::specialJunk2);
|
||||||
// jsonConfig[config_key::specialJunk3] = configMap.value(amnezia::config_key::specialJunk3);
|
jsonConfig[config_key::specialJunk3] = configMap.value(amnezia::config_key::specialJunk3);
|
||||||
// jsonConfig[config_key::specialJunk4] = configMap.value(amnezia::config_key::specialJunk4);
|
jsonConfig[config_key::specialJunk4] = configMap.value(amnezia::config_key::specialJunk4);
|
||||||
// jsonConfig[config_key::specialJunk5] = configMap.value(amnezia::config_key::specialJunk5);
|
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::mtu] =
|
jsonConfig[config_key::mtu] =
|
||||||
containerConfig.value(ProtocolProps::protoToString(Proto::Awg)).toObject().value(config_key::mtu).toString(protocols::awg::defaultMtu);
|
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 "";
|
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_CA_CERT", connData.caCert);
|
||||||
config.replace("$OPENVPN_CLIENT_CERT", connData.clientCert);
|
config.replace("$OPENVPN_CLIENT_CERT", connData.clientCert);
|
||||||
config.replace("$OPENVPN_PRIV_KEY", connData.privKey);
|
config.replace("$OPENVPN_PRIV_KEY", connData.privKey);
|
||||||
|
|
||||||
if (config.contains("$OPENVPN_TA_KEY")) {
|
if (config.contains("$OPENVPN_TA_KEY")) {
|
||||||
config.replace("$OPENVPN_TA_KEY", connData.taKey);
|
config.replace("$OPENVPN_TA_KEY", sanitizeStaticKey(connData.taKey));
|
||||||
} else {
|
} else {
|
||||||
config.replace("<tls-auth>", "");
|
config.replace("<tls-auth>", "");
|
||||||
config.replace("</tls-auth>", "");
|
config.replace("</tls-auth>", "");
|
||||||
@@ -117,7 +135,7 @@ QString OpenVpnConfigurator::processConfigWithLocalSettings(const QPair<QString,
|
|||||||
if (!isApiConfig) {
|
if (!isApiConfig) {
|
||||||
QRegularExpression regex("redirect-gateway.*");
|
QRegularExpression regex("redirect-gateway.*");
|
||||||
config.replace(regex, "");
|
config.replace(regex, "");
|
||||||
|
|
||||||
// We don't use secondary DNS if primary DNS is AmneziaDNS
|
// We don't use secondary DNS if primary DNS is AmneziaDNS
|
||||||
if (dns.first.contains(protocols::dns::amneziaDnsIp)) {
|
if (dns.first.contains(protocols::dns::amneziaDnsIp)) {
|
||||||
QRegularExpression dnsRegex("dhcp-option DNS " + dns.second);
|
QRegularExpression dnsRegex("dhcp-option DNS " + dns.second);
|
||||||
|
|||||||
@@ -103,7 +103,11 @@ WireguardConfigurator::ConnectionData WireguardConfigurator::prepareWireguardCon
|
|||||||
return connData;
|
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;
|
QString stdOut;
|
||||||
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
|
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {
|
||||||
stdOut += data + "\n";
|
stdOut += data + "\n";
|
||||||
@@ -161,15 +165,18 @@ WireguardConfigurator::ConnectionData WireguardConfigurator::prepareWireguardCon
|
|||||||
"AllowedIPs = %3/32\n\n")
|
"AllowedIPs = %3/32\n\n")
|
||||||
.arg(connData.clientPubKey, connData.pskKey, connData.clientIP);
|
.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);
|
libssh::ScpOverwriteMode::ScpAppendToExisting);
|
||||||
|
|
||||||
if (errorCode != ErrorCode::NoError) {
|
if (errorCode != ErrorCode::NoError) {
|
||||||
return connData;
|
return connData;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString script = QString("sudo docker exec -i $CONTAINER_NAME bash -c 'wg syncconf wg0 <(wg-quick strip %1)'")
|
bool isAwg = (container == DockerContainer::Awg2);
|
||||||
.arg(m_serverConfigPath);
|
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(
|
errorCode = m_serverController->runScript(
|
||||||
credentials,
|
credentials,
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ QString ContainerProps::containerToString(amnezia::DockerContainer c)
|
|||||||
return "none";
|
return "none";
|
||||||
if (c == DockerContainer::Cloak)
|
if (c == DockerContainer::Cloak)
|
||||||
return "amnezia-openvpn-cloak";
|
return "amnezia-openvpn-cloak";
|
||||||
|
if (c == DockerContainer::Awg)
|
||||||
|
return "amnezia-awg";
|
||||||
|
if (c == DockerContainer::Awg2)
|
||||||
|
return "amnezia-awg2";
|
||||||
QMetaEnum metaEnum = QMetaEnum::fromType<DockerContainer>();
|
QMetaEnum metaEnum = QMetaEnum::fromType<DockerContainer>();
|
||||||
QString containerKey = metaEnum.valueToKey(static_cast<int>(c));
|
QString containerKey = metaEnum.valueToKey(static_cast<int>(c));
|
||||||
|
|
||||||
@@ -41,7 +44,10 @@ QString ContainerProps::containerTypeToString(amnezia::DockerContainer c)
|
|||||||
return "none";
|
return "none";
|
||||||
if (c == DockerContainer::Ipsec)
|
if (c == DockerContainer::Ipsec)
|
||||||
return "ikev2";
|
return "ikev2";
|
||||||
|
if (c == DockerContainer::Awg)
|
||||||
|
return "awg";
|
||||||
|
if (c == DockerContainer::Awg2)
|
||||||
|
return "awg";
|
||||||
QMetaEnum metaEnum = QMetaEnum::fromType<DockerContainer>();
|
QMetaEnum metaEnum = QMetaEnum::fromType<DockerContainer>();
|
||||||
QString containerKey = metaEnum.valueToKey(static_cast<int>(c));
|
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::Socks5Proxy: return { Proto::Socks5Proxy };
|
||||||
|
|
||||||
|
case DockerContainer::Awg: return { Proto::Awg };
|
||||||
|
case DockerContainer::Awg2: return { Proto::Awg };
|
||||||
default: return { defaultProtocol(container) };
|
default: return { defaultProtocol(container) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,6 +102,7 @@ QMap<DockerContainer, QString> ContainerProps::containerHumanNames()
|
|||||||
{ DockerContainer::Cloak, "OpenVPN over Cloak" },
|
{ DockerContainer::Cloak, "OpenVPN over Cloak" },
|
||||||
{ DockerContainer::WireGuard, "WireGuard" },
|
{ DockerContainer::WireGuard, "WireGuard" },
|
||||||
{ DockerContainer::Awg, "AmneziaWG" },
|
{ DockerContainer::Awg, "AmneziaWG" },
|
||||||
|
{ DockerContainer::Awg2, "AmneziaWG" },
|
||||||
{ DockerContainer::Xray, "XRay" },
|
{ DockerContainer::Xray, "XRay" },
|
||||||
{ DockerContainer::Ipsec, QObject::tr("IPsec") },
|
{ DockerContainer::Ipsec, QObject::tr("IPsec") },
|
||||||
{ DockerContainer::SSXray, "Shadowsocks"},
|
{ DockerContainer::SSXray, "Shadowsocks"},
|
||||||
@@ -120,6 +129,9 @@ QMap<DockerContainer, QString> ContainerProps::containerDescriptions()
|
|||||||
{ DockerContainer::Awg,
|
{ DockerContainer::Awg,
|
||||||
QObject::tr("AmneziaWG is a special protocol from Amnezia based on WireGuard. "
|
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.") },
|
"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,
|
{ DockerContainer::Xray,
|
||||||
QObject::tr("XRay with REALITY masks VPN traffic as web traffic and protects against active probing. "
|
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.") },
|
"It is highly resistant to detection and offers high speed.") },
|
||||||
@@ -182,7 +194,7 @@ QMap<DockerContainer, QString> ContainerProps::containerDetailedDescriptions()
|
|||||||
"* Minimal configuration required\n"
|
"* Minimal configuration required\n"
|
||||||
"* Easily detected by DPI systems (susceptible to blocking)\n"
|
"* Easily detected by DPI systems (susceptible to blocking)\n"
|
||||||
"* Operates over UDP protocol") },
|
"* Operates over UDP protocol") },
|
||||||
{ DockerContainer::Awg,
|
{ DockerContainer::Awg2,
|
||||||
QObject::tr("AmneziaWG is a modern VPN protocol based on WireGuard, "
|
QObject::tr("AmneziaWG is a modern VPN protocol based on WireGuard, "
|
||||||
"combining simplified architecture with high performance across all devices. "
|
"combining simplified architecture with high performance across all devices. "
|
||||||
"It addresses WireGuard's main vulnerability (easy detection by DPI systems) through advanced obfuscation techniques, "
|
"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::Cloak: return Proto::Cloak;
|
||||||
case DockerContainer::ShadowSocks: return Proto::ShadowSocks;
|
case DockerContainer::ShadowSocks: return Proto::ShadowSocks;
|
||||||
case DockerContainer::WireGuard: return Proto::WireGuard;
|
case DockerContainer::WireGuard: return Proto::WireGuard;
|
||||||
|
case DockerContainer::Awg2: return Proto::Awg;
|
||||||
case DockerContainer::Awg: return Proto::Awg;
|
case DockerContainer::Awg: return Proto::Awg;
|
||||||
case DockerContainer::Xray: return Proto::Xray;
|
case DockerContainer::Xray: return Proto::Xray;
|
||||||
case DockerContainer::Ipsec: return Proto::Ikev2;
|
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)
|
bool ContainerProps::isSupportedByCurrentPlatform(DockerContainer c)
|
||||||
{
|
{
|
||||||
#ifdef Q_OS_WINDOWS
|
#ifdef Q_OS_WINDOWS
|
||||||
@@ -265,6 +287,7 @@ bool ContainerProps::isSupportedByCurrentPlatform(DockerContainer c)
|
|||||||
switch (c) {
|
switch (c) {
|
||||||
case DockerContainer::WireGuard: return true;
|
case DockerContainer::WireGuard: return true;
|
||||||
case DockerContainer::OpenVpn: return true;
|
case DockerContainer::OpenVpn: return true;
|
||||||
|
case DockerContainer::Awg2: return true;
|
||||||
case DockerContainer::Awg: return true;
|
case DockerContainer::Awg: return true;
|
||||||
case DockerContainer::Xray: return true;
|
case DockerContainer::Xray: return true;
|
||||||
case DockerContainer::Cloak: return true;
|
case DockerContainer::Cloak: return true;
|
||||||
@@ -275,13 +298,14 @@ bool ContainerProps::isSupportedByCurrentPlatform(DockerContainer c)
|
|||||||
}
|
}
|
||||||
|
|
||||||
#elif defined(MACOS_NE)
|
#elif defined(MACOS_NE)
|
||||||
// macOS build using Network Extension – hide OpenVPN-based containers
|
// macOS build using Network Extension – allow OpenVPN for parity with iOS.
|
||||||
switch (c) {
|
switch (c) {
|
||||||
|
case DockerContainer::OpenVpn: return true;
|
||||||
case DockerContainer::WireGuard: return true;
|
case DockerContainer::WireGuard: return true;
|
||||||
|
case DockerContainer::Awg2: return true;
|
||||||
case DockerContainer::Awg: return true;
|
case DockerContainer::Awg: return true;
|
||||||
case DockerContainer::Xray: return true;
|
case DockerContainer::Xray: return true;
|
||||||
case DockerContainer::SSXray: return true;
|
case DockerContainer::SSXray: return true;
|
||||||
case DockerContainer::OpenVpn:
|
|
||||||
case DockerContainer::Cloak:
|
case DockerContainer::Cloak:
|
||||||
case DockerContainer::ShadowSocks:
|
case DockerContainer::ShadowSocks:
|
||||||
return false;
|
return false;
|
||||||
@@ -300,6 +324,7 @@ bool ContainerProps::isSupportedByCurrentPlatform(DockerContainer c)
|
|||||||
case DockerContainer::WireGuard: return true;
|
case DockerContainer::WireGuard: return true;
|
||||||
case DockerContainer::OpenVpn: return true;
|
case DockerContainer::OpenVpn: return true;
|
||||||
case DockerContainer::ShadowSocks: return false;
|
case DockerContainer::ShadowSocks: return false;
|
||||||
|
case DockerContainer::Awg2: return true;
|
||||||
case DockerContainer::Awg: return true;
|
case DockerContainer::Awg: return true;
|
||||||
case DockerContainer::Cloak: return true;
|
case DockerContainer::Cloak: return true;
|
||||||
case DockerContainer::Xray: return true;
|
case DockerContainer::Xray: return true;
|
||||||
@@ -329,7 +354,7 @@ QStringList ContainerProps::fixedPortsForContainer(DockerContainer c)
|
|||||||
bool ContainerProps::isEasySetupContainer(DockerContainer container)
|
bool ContainerProps::isEasySetupContainer(DockerContainer container)
|
||||||
{
|
{
|
||||||
switch (container) {
|
switch (container) {
|
||||||
case DockerContainer::Awg: return true;
|
case DockerContainer::Awg2: return true;
|
||||||
default: return false;
|
default: return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -337,7 +362,7 @@ bool ContainerProps::isEasySetupContainer(DockerContainer container)
|
|||||||
QString ContainerProps::easySetupHeader(DockerContainer container)
|
QString ContainerProps::easySetupHeader(DockerContainer container)
|
||||||
{
|
{
|
||||||
switch (container) {
|
switch (container) {
|
||||||
case DockerContainer::Awg: return tr("Automatic");
|
case DockerContainer::Awg2: return tr("Automatic");
|
||||||
default: return "";
|
default: return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -345,7 +370,7 @@ QString ContainerProps::easySetupHeader(DockerContainer container)
|
|||||||
QString ContainerProps::easySetupDescription(DockerContainer container)
|
QString ContainerProps::easySetupDescription(DockerContainer container)
|
||||||
{
|
{
|
||||||
switch (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.");
|
"It provides high connection speed and ensures stable operation even in the most challenging network conditions.");
|
||||||
default: return "";
|
default: return "";
|
||||||
}
|
}
|
||||||
@@ -354,7 +379,7 @@ QString ContainerProps::easySetupDescription(DockerContainer container)
|
|||||||
int ContainerProps::easySetupOrder(DockerContainer container)
|
int ContainerProps::easySetupOrder(DockerContainer container)
|
||||||
{
|
{
|
||||||
switch (container) {
|
switch (container) {
|
||||||
case DockerContainer::Awg: return 1;
|
case DockerContainer::Awg2: return 1;
|
||||||
default: return 0;
|
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)
|
QJsonObject ContainerProps::getProtocolConfigFromContainer(const Proto protocol, const QJsonObject &containerConfig)
|
||||||
{
|
{
|
||||||
QString protocolConfigString = containerConfig.value(ProtocolProps::protoToString(protocol))
|
QString protocolConfigString = containerConfig.value(ProtocolProps::protoToString(protocol))
|
||||||
@@ -387,7 +418,7 @@ int ContainerProps::installPageOrder(DockerContainer container)
|
|||||||
case DockerContainer::Cloak: return 5;
|
case DockerContainer::Cloak: return 5;
|
||||||
case DockerContainer::ShadowSocks: return 6;
|
case DockerContainer::ShadowSocks: return 6;
|
||||||
case DockerContainer::WireGuard: return 2;
|
case DockerContainer::WireGuard: return 2;
|
||||||
case DockerContainer::Awg: return 1;
|
case DockerContainer::Awg2: return 1;
|
||||||
case DockerContainer::Xray: return 3;
|
case DockerContainer::Xray: return 3;
|
||||||
case DockerContainer::Ipsec: return 7;
|
case DockerContainer::Ipsec: return 7;
|
||||||
case DockerContainer::SSXray: return 8;
|
case DockerContainer::SSXray: return 8;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ namespace amnezia
|
|||||||
enum DockerContainer {
|
enum DockerContainer {
|
||||||
None = 0,
|
None = 0,
|
||||||
Awg,
|
Awg,
|
||||||
|
Awg2,
|
||||||
WireGuard,
|
WireGuard,
|
||||||
OpenVpn,
|
OpenVpn,
|
||||||
Cloak,
|
Cloak,
|
||||||
@@ -45,6 +46,7 @@ namespace amnezia
|
|||||||
Q_INVOKABLE static amnezia::DockerContainer containerFromString(const QString &container);
|
Q_INVOKABLE static amnezia::DockerContainer containerFromString(const QString &container);
|
||||||
Q_INVOKABLE static QString containerToString(amnezia::DockerContainer container);
|
Q_INVOKABLE static QString containerToString(amnezia::DockerContainer container);
|
||||||
Q_INVOKABLE static QString containerTypeToString(amnezia::DockerContainer c);
|
Q_INVOKABLE static QString containerTypeToString(amnezia::DockerContainer c);
|
||||||
|
Q_INVOKABLE static QString containerTypeToProtocolString(amnezia::DockerContainer c);
|
||||||
|
|
||||||
Q_INVOKABLE static QList<amnezia::DockerContainer> allContainers();
|
Q_INVOKABLE static QList<amnezia::DockerContainer> allContainers();
|
||||||
|
|
||||||
@@ -71,6 +73,9 @@ namespace amnezia
|
|||||||
|
|
||||||
static bool isShareable(amnezia::DockerContainer container);
|
static bool isShareable(amnezia::DockerContainer container);
|
||||||
|
|
||||||
|
static bool isAwgContainer(amnezia::DockerContainer container);
|
||||||
|
|
||||||
|
|
||||||
static QJsonObject getProtocolConfigFromContainer(const amnezia::Proto protocol, const QJsonObject &containerConfig);
|
static QJsonObject getProtocolConfigFromContainer(const amnezia::Proto protocol, const QJsonObject &containerConfig);
|
||||||
|
|
||||||
static int installPageOrder(amnezia::DockerContainer container);
|
static int installPageOrder(amnezia::DockerContainer container);
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ namespace apiDefs
|
|||||||
AmneziaPremiumV1,
|
AmneziaPremiumV1,
|
||||||
AmneziaPremiumV2,
|
AmneziaPremiumV2,
|
||||||
SelfHosted,
|
SelfHosted,
|
||||||
ExternalPremium
|
ExternalPremium,
|
||||||
|
ExternalTrial
|
||||||
};
|
};
|
||||||
|
|
||||||
enum ConfigSource {
|
enum ConfigSource {
|
||||||
@@ -32,6 +33,7 @@ namespace apiDefs
|
|||||||
constexpr QLatin1String stackType("stack_type");
|
constexpr QLatin1String stackType("stack_type");
|
||||||
constexpr QLatin1String serviceType("service_type");
|
constexpr QLatin1String serviceType("service_type");
|
||||||
constexpr QLatin1String cliVersion("cli_version");
|
constexpr QLatin1String cliVersion("cli_version");
|
||||||
|
constexpr QLatin1String cliName("cli_name");
|
||||||
constexpr QLatin1String supportedProtocols("supported_protocols");
|
constexpr QLatin1String supportedProtocols("supported_protocols");
|
||||||
|
|
||||||
constexpr QLatin1String vpnKey("vpn_key");
|
constexpr QLatin1String vpnKey("vpn_key");
|
||||||
@@ -47,12 +49,20 @@ namespace apiDefs
|
|||||||
constexpr QLatin1String serverCountryName("server_country_name");
|
constexpr QLatin1String serverCountryName("server_country_name");
|
||||||
|
|
||||||
constexpr QLatin1String osVersion("os_version");
|
constexpr QLatin1String osVersion("os_version");
|
||||||
|
constexpr QLatin1String appLanguage("app_language");
|
||||||
|
|
||||||
constexpr QLatin1String availableCountries("available_countries");
|
constexpr QLatin1String availableCountries("available_countries");
|
||||||
constexpr QLatin1String activeDeviceCount("active_device_count");
|
constexpr QLatin1String activeDeviceCount("active_device_count");
|
||||||
constexpr QLatin1String maxDeviceCount("max_device_count");
|
constexpr QLatin1String maxDeviceCount("max_device_count");
|
||||||
constexpr QLatin1String subscriptionEndDate("subscription_end_date");
|
constexpr QLatin1String subscriptionEndDate("subscription_end_date");
|
||||||
|
constexpr QLatin1String subscriptionExpiredByServer("subscription_expired_by_server");
|
||||||
|
constexpr QLatin1String subscriptionStatus("subscription_status");
|
||||||
|
constexpr QLatin1String subscription("subscription");
|
||||||
|
constexpr QLatin1String endDate("end_date");
|
||||||
constexpr QLatin1String issuedConfigs("issued_configs");
|
constexpr QLatin1String issuedConfigs("issued_configs");
|
||||||
|
constexpr QLatin1String subscriptionDescription("subscription_description");
|
||||||
|
constexpr QLatin1String termsOfUseUrl("terms_of_use_url");
|
||||||
|
constexpr QLatin1String privacyPolicyUrl("privacy_policy_url");
|
||||||
|
|
||||||
constexpr QLatin1String supportInfo("support_info");
|
constexpr QLatin1String supportInfo("support_info");
|
||||||
constexpr QLatin1String email("email");
|
constexpr QLatin1String email("email");
|
||||||
@@ -64,6 +74,19 @@ namespace apiDefs
|
|||||||
constexpr QLatin1String id("id");
|
constexpr QLatin1String id("id");
|
||||||
constexpr QLatin1String orderId("order_id");
|
constexpr QLatin1String orderId("order_id");
|
||||||
constexpr QLatin1String migrationCode("migration_code");
|
constexpr QLatin1String migrationCode("migration_code");
|
||||||
|
|
||||||
|
constexpr QLatin1String transactionId("transaction_id");
|
||||||
|
constexpr QLatin1String isTestPurchase("is_test_purchase");
|
||||||
|
constexpr QLatin1String isInAppPurchase("is_in_app_purchase");
|
||||||
|
|
||||||
|
constexpr QLatin1String userCountryCode("user_country_code");
|
||||||
|
|
||||||
|
constexpr QLatin1String serviceInfo("service_info");
|
||||||
|
constexpr QLatin1String isAdVisible("is_ad_visible");
|
||||||
|
constexpr QLatin1String isRenewalAvailable("is_renewal_available");
|
||||||
|
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 = 12 * 1000; // 12 secs
|
||||||
|
|||||||
@@ -1,12 +1,35 @@
|
|||||||
#include "apiUtils.h"
|
#include "apiUtils.h"
|
||||||
|
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
#include <QJsonValue>
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
const QByteArray AMNEZIA_CONFIG_SIGNATURE = QByteArray::fromHex("000000ff");
|
const QByteArray AMNEZIA_CONFIG_SIGNATURE = QByteArray::fromHex("000000ff");
|
||||||
|
|
||||||
|
constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?");
|
||||||
|
constexpr QLatin1String trialAlreadyUsedMessage("trial subscription already used");
|
||||||
|
|
||||||
|
QDateTime subscriptionEndUtcFromString(const QString &subscriptionEndDate)
|
||||||
|
{
|
||||||
|
if (subscriptionEndDate.isEmpty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
QDateTime endDate = QDateTime::fromString(subscriptionEndDate, Qt::ISODateWithMs).toUTC();
|
||||||
|
if (!endDate.isValid()) {
|
||||||
|
endDate = QDateTime::fromString(subscriptionEndDate, Qt::ISODate).toUTC();
|
||||||
|
}
|
||||||
|
return endDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString apiErrorMessageFromJson(const QJsonObject &jsonObj)
|
||||||
|
{
|
||||||
|
const QJsonValue value = jsonObj.value(QStringLiteral("message"));
|
||||||
|
return value.isString() ? value.toString().trimmed() : QString();
|
||||||
|
}
|
||||||
|
|
||||||
QString escapeUnicode(const QString &input)
|
QString escapeUnicode(const QString &input)
|
||||||
{
|
{
|
||||||
QString output;
|
QString output;
|
||||||
@@ -23,9 +46,30 @@ namespace
|
|||||||
|
|
||||||
bool apiUtils::isSubscriptionExpired(const QString &subscriptionEndDate)
|
bool apiUtils::isSubscriptionExpired(const QString &subscriptionEndDate)
|
||||||
{
|
{
|
||||||
QDateTime now = QDateTime::currentDateTimeUtc();
|
if (subscriptionEndDate.isEmpty()) {
|
||||||
QDateTime endDate = QDateTime::fromString(subscriptionEndDate, Qt::ISODateWithMs);
|
return false;
|
||||||
return endDate < now;
|
}
|
||||||
|
const QDateTime endDate = subscriptionEndUtcFromString(subscriptionEndDate);
|
||||||
|
if (!endDate.isValid()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return endDate <= QDateTime::currentDateTimeUtc();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool apiUtils::isSubscriptionExpiringSoon(const QString &subscriptionEndDate, int withinDays)
|
||||||
|
{
|
||||||
|
if (subscriptionEndDate.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const QDateTime endDate = subscriptionEndUtcFromString(subscriptionEndDate);
|
||||||
|
if (!endDate.isValid()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const QDateTime nowUtc = QDateTime::currentDateTimeUtc();
|
||||||
|
if (endDate <= nowUtc) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return endDate <= nowUtc.addDays(withinDays);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool apiUtils::isServerFromApi(const QJsonObject &serverConfigObject)
|
bool apiUtils::isServerFromApi(const QJsonObject &serverConfigObject)
|
||||||
@@ -59,6 +103,7 @@ apiDefs::ConfigType apiUtils::getConfigType(const QJsonObject &serverConfigObjec
|
|||||||
constexpr QLatin1String servicePremium("amnezia-premium");
|
constexpr QLatin1String servicePremium("amnezia-premium");
|
||||||
constexpr QLatin1String serviceFree("amnezia-free");
|
constexpr QLatin1String serviceFree("amnezia-free");
|
||||||
constexpr QLatin1String serviceExternalPremium("external-premium");
|
constexpr QLatin1String serviceExternalPremium("external-premium");
|
||||||
|
constexpr QLatin1String serviceExternalTrial("external-trial");
|
||||||
|
|
||||||
auto apiConfigObject = serverConfigObject.value(apiDefs::key::apiConfig).toObject();
|
auto apiConfigObject = serverConfigObject.value(apiDefs::key::apiConfig).toObject();
|
||||||
auto serviceType = apiConfigObject.value(apiDefs::key::serviceType).toString();
|
auto serviceType = apiConfigObject.value(apiDefs::key::serviceType).toString();
|
||||||
@@ -69,6 +114,8 @@ apiDefs::ConfigType apiUtils::getConfigType(const QJsonObject &serverConfigObjec
|
|||||||
return apiDefs::ConfigType::AmneziaFreeV3;
|
return apiDefs::ConfigType::AmneziaFreeV3;
|
||||||
} else if (serviceType == serviceExternalPremium) {
|
} else if (serviceType == serviceExternalPremium) {
|
||||||
return apiDefs::ConfigType::ExternalPremium;
|
return apiDefs::ConfigType::ExternalPremium;
|
||||||
|
} else if (serviceType == serviceExternalTrial) {
|
||||||
|
return apiDefs::ConfigType::ExternalTrial;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
@@ -82,46 +129,73 @@ apiDefs::ConfigSource apiUtils::getConfigSource(const QJsonObject &serverConfigO
|
|||||||
return static_cast<apiDefs::ConfigSource>(serverConfigObject.value(apiDefs::key::configVersion).toInt());
|
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 httpStatusCodeConflict = 409;
|
||||||
const int httpStatusCodeNotFound = 404;
|
const int httpStatusCodeNotFound = 404;
|
||||||
|
const int httpStatusCodeNotImplemented = 501;
|
||||||
|
const int httpStatusCodePaymentRequired = 402;
|
||||||
|
const int httpStatusCodeUnprocessableEntity = 422;
|
||||||
|
|
||||||
if (!sslErrors.empty()) {
|
if (!sslErrors.empty()) {
|
||||||
qDebug().noquote() << sslErrors;
|
qDebug().noquote() << sslErrors;
|
||||||
return amnezia::ErrorCode::ApiConfigSslError;
|
return amnezia::ErrorCode::ApiConfigSslError;
|
||||||
} else if (reply->error() == QNetworkReply::NoError) {
|
}
|
||||||
|
if (replyError == QNetworkReply::NoError) {
|
||||||
return amnezia::ErrorCode::NoError;
|
return amnezia::ErrorCode::NoError;
|
||||||
} else if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError
|
}
|
||||||
|| reply->error() == QNetworkReply::NetworkError::TimeoutError) {
|
if (replyError == QNetworkReply::NetworkError::OperationCanceledError
|
||||||
qDebug() << reply->error();
|
|| replyError == QNetworkReply::NetworkError::TimeoutError) {
|
||||||
|
qDebug() << replyError;
|
||||||
return amnezia::ErrorCode::ApiConfigTimeoutError;
|
return amnezia::ErrorCode::ApiConfigTimeoutError;
|
||||||
} else if (reply->error() == QNetworkReply::NetworkError::OperationNotImplementedError) {
|
}
|
||||||
qDebug() << reply->error();
|
if (replyError == QNetworkReply::NetworkError::OperationNotImplementedError) {
|
||||||
|
qDebug() << replyError;
|
||||||
return amnezia::ErrorCode::ApiUpdateRequestError;
|
return amnezia::ErrorCode::ApiUpdateRequestError;
|
||||||
} else {
|
}
|
||||||
QString err = reply->errorString();
|
|
||||||
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
qDebug() << QString::fromUtf8(responseBody);
|
||||||
qDebug() << QString::fromUtf8(reply->readAll());
|
qDebug() << replyError;
|
||||||
qDebug() << reply->error();
|
qDebug() << httpStatusCode;
|
||||||
qDebug() << err;
|
|
||||||
qDebug() << httpStatusCode;
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
||||||
if (httpStatusCode == httpStatusCodeConflict) {
|
if (jsonDoc.isObject()) {
|
||||||
|
QJsonObject jsonObj = jsonDoc.object();
|
||||||
|
const int httpStatusFromBody = jsonObj.value(QStringLiteral("http_status")).toInt(-1);
|
||||||
|
if (httpStatusFromBody == httpStatusCodeConflict) {
|
||||||
|
if (apiErrorMessageFromJson(jsonObj).contains(trialAlreadyUsedMessage, Qt::CaseInsensitive)) {
|
||||||
|
return amnezia::ErrorCode::ApiTrialAlreadyUsedError;
|
||||||
|
}
|
||||||
return amnezia::ErrorCode::ApiConfigLimitError;
|
return amnezia::ErrorCode::ApiConfigLimitError;
|
||||||
} else if (httpStatusCode == httpStatusCodeNotFound) {
|
}
|
||||||
|
if (httpStatusFromBody == httpStatusCodeNotFound) {
|
||||||
return amnezia::ErrorCode::ApiNotFoundError;
|
return amnezia::ErrorCode::ApiNotFoundError;
|
||||||
}
|
}
|
||||||
|
if (httpStatusFromBody == httpStatusCodeNotImplemented) {
|
||||||
|
return amnezia::ErrorCode::ApiUpdateRequestError;
|
||||||
|
}
|
||||||
|
if (httpStatusFromBody == httpStatusCodeUnprocessableEntity) {
|
||||||
|
if (apiErrorMessageFromJson(jsonObj) == unprocessableSubscriptionMessage) {
|
||||||
|
return amnezia::ErrorCode::ApiSubscriptionExpiredError;
|
||||||
|
}
|
||||||
|
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||||
|
}
|
||||||
|
if (httpStatusFromBody == httpStatusCodePaymentRequired) {
|
||||||
|
return amnezia::ErrorCode::ApiSubscriptionNotActiveError;
|
||||||
|
}
|
||||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||||
}
|
}
|
||||||
|
|
||||||
qDebug() << "something went wrong";
|
qDebug() << "something went wrong";
|
||||||
return amnezia::ErrorCode::InternalError;
|
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool apiUtils::isPremiumServer(const QJsonObject &serverConfigObject)
|
bool apiUtils::isPremiumServer(const QJsonObject &serverConfigObject)
|
||||||
{
|
{
|
||||||
static const QSet<apiDefs::ConfigType> premiumTypes = { apiDefs::ConfigType::AmneziaPremiumV1, apiDefs::ConfigType::AmneziaPremiumV2,
|
static const QSet<apiDefs::ConfigType> premiumTypes = { apiDefs::ConfigType::AmneziaPremiumV1, apiDefs::ConfigType::AmneziaPremiumV2,
|
||||||
apiDefs::ConfigType::ExternalPremium };
|
apiDefs::ConfigType::ExternalPremium, apiDefs::ConfigType::ExternalTrial };
|
||||||
return premiumTypes.contains(getConfigType(serverConfigObject));
|
return premiumTypes.contains(getConfigType(serverConfigObject));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,3 +236,53 @@ QString apiUtils::getPremiumV1VpnKey(const QJsonObject &serverConfigObject)
|
|||||||
|
|
||||||
return QString("vpn://%1").arg(QString(signedData.toBase64(QByteArray::Base64UrlEncoding)));
|
return QString("vpn://%1").arg(QString(signedData.toBase64(QByteArray::Base64UrlEncoding)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString apiUtils::getPremiumV2VpnKey(const QJsonObject &serverConfigObject)
|
||||||
|
{
|
||||||
|
auto configType = apiUtils::getConfigType(serverConfigObject);
|
||||||
|
if (configType != apiDefs::ConfigType::AmneziaPremiumV2 && configType != apiDefs::ConfigType::ExternalPremium
|
||||||
|
&& configType != apiDefs::ConfigType::ExternalTrial) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,14 +13,19 @@ namespace apiUtils
|
|||||||
|
|
||||||
bool isSubscriptionExpired(const QString &subscriptionEndDate);
|
bool isSubscriptionExpired(const QString &subscriptionEndDate);
|
||||||
|
|
||||||
|
bool isSubscriptionExpiringSoon(const QString &subscriptionEndDate, int withinDays = 30);
|
||||||
|
|
||||||
bool isPremiumServer(const QJsonObject &serverConfigObject);
|
bool isPremiumServer(const QJsonObject &serverConfigObject);
|
||||||
|
|
||||||
apiDefs::ConfigType getConfigType(const QJsonObject &serverConfigObject);
|
apiDefs::ConfigType getConfigType(const QJsonObject &serverConfigObject);
|
||||||
apiDefs::ConfigSource getConfigSource(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 getPremiumV1VpnKey(const QJsonObject &serverConfigObject);
|
||||||
|
QString getPremiumV2VpnKey(const QJsonObject &serverConfigObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif // APIUTILS_H
|
#endif // APIUTILS_H
|
||||||
|
|||||||
@@ -91,6 +91,12 @@ void CoreController::initModels()
|
|||||||
m_apiServicesModel.reset(new ApiServicesModel(this));
|
m_apiServicesModel.reset(new ApiServicesModel(this));
|
||||||
m_engine->rootContext()->setContextProperty("ApiServicesModel", m_apiServicesModel.get());
|
m_engine->rootContext()->setContextProperty("ApiServicesModel", m_apiServicesModel.get());
|
||||||
|
|
||||||
|
m_apiSubscriptionPlansModel.reset(new ApiSubscriptionPlansModel(this));
|
||||||
|
m_engine->rootContext()->setContextProperty("ApiSubscriptionPlansModel", m_apiSubscriptionPlansModel.get());
|
||||||
|
|
||||||
|
m_apiBenefitsModel.reset(new ApiBenefitsModel(this));
|
||||||
|
m_engine->rootContext()->setContextProperty("ApiBenefitsModel", m_apiBenefitsModel.get());
|
||||||
|
|
||||||
m_apiCountryModel.reset(new ApiCountryModel(this));
|
m_apiCountryModel.reset(new ApiCountryModel(this));
|
||||||
m_engine->rootContext()->setContextProperty("ApiCountryModel", m_apiCountryModel.get());
|
m_engine->rootContext()->setContextProperty("ApiCountryModel", m_apiCountryModel.get());
|
||||||
|
|
||||||
@@ -99,6 +105,9 @@ void CoreController::initModels()
|
|||||||
|
|
||||||
m_apiDevicesModel.reset(new ApiDevicesModel(m_settings, this));
|
m_apiDevicesModel.reset(new ApiDevicesModel(m_settings, this));
|
||||||
m_engine->rootContext()->setContextProperty("ApiDevicesModel", m_apiDevicesModel.get());
|
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()
|
void CoreController::initControllers()
|
||||||
@@ -132,7 +141,7 @@ void CoreController::initControllers()
|
|||||||
new SettingsController(m_serversModel, m_containersModel, m_languageModel, m_sitesModel, m_appSplitTunnelingModel, m_settings));
|
new SettingsController(m_serversModel, m_containersModel, m_languageModel, m_sitesModel, m_appSplitTunnelingModel, m_settings));
|
||||||
m_engine->rootContext()->setContextProperty("SettingsController", m_settingsController.get());
|
m_engine->rootContext()->setContextProperty("SettingsController", m_settingsController.get());
|
||||||
|
|
||||||
m_sitesController.reset(new SitesController(m_settings, m_vpnConnection, m_sitesModel));
|
m_sitesController.reset(new SitesController(m_settings, m_sitesModel));
|
||||||
m_engine->rootContext()->setContextProperty("SitesController", m_sitesController.get());
|
m_engine->rootContext()->setContextProperty("SitesController", m_sitesController.get());
|
||||||
|
|
||||||
m_allowedDnsController.reset(new AllowedDnsController(m_settings, m_allowedDnsModel));
|
m_allowedDnsController.reset(new AllowedDnsController(m_settings, m_allowedDnsModel));
|
||||||
@@ -148,11 +157,14 @@ void CoreController::initControllers()
|
|||||||
new ApiSettingsController(m_serversModel, m_apiAccountInfoModel, m_apiCountryModel, m_apiDevicesModel, m_settings));
|
new ApiSettingsController(m_serversModel, m_apiAccountInfoModel, m_apiCountryModel, m_apiDevicesModel, m_settings));
|
||||||
m_engine->rootContext()->setContextProperty("ApiSettingsController", m_apiSettingsController.get());
|
m_engine->rootContext()->setContextProperty("ApiSettingsController", m_apiSettingsController.get());
|
||||||
|
|
||||||
m_apiConfigsController.reset(new ApiConfigsController(m_serversModel, m_apiServicesModel, m_settings));
|
m_apiConfigsController.reset(
|
||||||
|
new ApiConfigsController(m_serversModel, m_apiServicesModel, m_apiSubscriptionPlansModel, m_apiBenefitsModel, m_settings));
|
||||||
m_engine->rootContext()->setContextProperty("ApiConfigsController", m_apiConfigsController.get());
|
m_engine->rootContext()->setContextProperty("ApiConfigsController", m_apiConfigsController.get());
|
||||||
|
connect(m_apiConfigsController.get(), &ApiConfigsController::subscriptionRefreshNeeded,
|
||||||
|
this, [this]() { m_apiSettingsController->getAccountInfo(false); });
|
||||||
|
|
||||||
m_apiPremV1MigrationController.reset(new ApiPremV1MigrationController(m_serversModel, m_settings, this));
|
m_apiNewsController.reset(new ApiNewsController(m_newsModel, m_settings, m_serversModel, this));
|
||||||
m_engine->rootContext()->setContextProperty("ApiPremV1MigrationController", m_apiPremV1MigrationController.get());
|
m_engine->rootContext()->setContextProperty("ApiNewsController", m_apiNewsController.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
void CoreController::initAndroidController()
|
void CoreController::initAndroidController()
|
||||||
@@ -225,8 +237,6 @@ void CoreController::initSignalHandlers()
|
|||||||
initAutoConnectHandler();
|
initAutoConnectHandler();
|
||||||
initAmneziaDnsToggledHandler();
|
initAmneziaDnsToggledHandler();
|
||||||
initPrepareConfigHandler();
|
initPrepareConfigHandler();
|
||||||
initImportPremiumV2VpnKeyHandler();
|
|
||||||
initShowMigrationDrawerHandler();
|
|
||||||
initStrictKillSwitchHandler();
|
initStrictKillSwitchHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,6 +326,11 @@ void CoreController::initContainerModelUpdateHandler()
|
|||||||
connect(m_serversModel.get(), &ServersModel::containersUpdated, m_containersModel.get(), &ContainersModel::updateModel);
|
connect(m_serversModel.get(), &ServersModel::containersUpdated, m_containersModel.get(), &ContainersModel::updateModel);
|
||||||
connect(m_serversModel.get(), &ServersModel::defaultServerContainersUpdated, m_defaultServerContainersModel.get(),
|
connect(m_serversModel.get(), &ServersModel::defaultServerContainersUpdated, m_defaultServerContainersModel.get(),
|
||||||
&ContainersModel::updateModel);
|
&ContainersModel::updateModel);
|
||||||
|
connect(m_serversModel.get(), &ServersModel::gatewayStacksExpanded, this, [this]() {
|
||||||
|
if (m_serversModel->hasServersFromGatewayApi()) {
|
||||||
|
m_apiNewsController->fetchNews(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
m_serversModel->resetModel();
|
m_serversModel->resetModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,7 +377,11 @@ void CoreController::initPrepareConfigHandler()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!m_installController->isConfigValid()) {
|
m_installController->validateConfig();
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(m_installController.get(), &InstallController::configValidated, this, [this](bool isValid) {
|
||||||
|
if (!isValid) {
|
||||||
emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected);
|
emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -371,25 +390,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()
|
void CoreController::initStrictKillSwitchHandler()
|
||||||
{
|
{
|
||||||
connect(m_settingsController.get(), &SettingsController::strictKillSwitchEnabledChanged, m_vpnConnection.get(),
|
connect(m_settingsController.get(), &SettingsController::strictKillSwitchEnabledChanged, m_vpnConnection.get(),
|
||||||
@@ -400,3 +400,22 @@ QSharedPointer<PageController> CoreController::pageController() const
|
|||||||
{
|
{
|
||||||
return m_pageController;
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
#include "ui/controllers/api/apiConfigsController.h"
|
#include "ui/controllers/api/apiConfigsController.h"
|
||||||
#include "ui/controllers/api/apiSettingsController.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/appSplitTunnelingController.h"
|
||||||
#include "ui/controllers/allowedDnsController.h"
|
#include "ui/controllers/allowedDnsController.h"
|
||||||
#include "ui/controllers/connectionController.h"
|
#include "ui/controllers/connectionController.h"
|
||||||
@@ -32,9 +32,11 @@
|
|||||||
#include "ui/models/protocols/ikev2ConfigModel.h"
|
#include "ui/models/protocols/ikev2ConfigModel.h"
|
||||||
#endif
|
#endif
|
||||||
#include "ui/models/api/apiAccountInfoModel.h"
|
#include "ui/models/api/apiAccountInfoModel.h"
|
||||||
|
#include "ui/models/api/apiBenefitsModel.h"
|
||||||
#include "ui/models/api/apiCountryModel.h"
|
#include "ui/models/api/apiCountryModel.h"
|
||||||
#include "ui/models/api/apiDevicesModel.h"
|
#include "ui/models/api/apiDevicesModel.h"
|
||||||
#include "ui/models/api/apiServicesModel.h"
|
#include "ui/models/api/apiServicesModel.h"
|
||||||
|
#include "ui/models/api/apiSubscriptionPlansModel.h"
|
||||||
#include "ui/models/appSplitTunnelingModel.h"
|
#include "ui/models/appSplitTunnelingModel.h"
|
||||||
#include "ui/models/clientManagementModel.h"
|
#include "ui/models/clientManagementModel.h"
|
||||||
#include "ui/models/protocols/awgConfigModel.h"
|
#include "ui/models/protocols/awgConfigModel.h"
|
||||||
@@ -47,6 +49,7 @@
|
|||||||
#include "ui/models/services/sftpConfigModel.h"
|
#include "ui/models/services/sftpConfigModel.h"
|
||||||
#include "ui/models/services/socks5ProxyConfigModel.h"
|
#include "ui/models/services/socks5ProxyConfigModel.h"
|
||||||
#include "ui/models/sites_model.h"
|
#include "ui/models/sites_model.h"
|
||||||
|
#include "ui/models/newsModel.h"
|
||||||
|
|
||||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||||
#include "ui/notificationhandler.h"
|
#include "ui/notificationhandler.h"
|
||||||
@@ -63,6 +66,9 @@ public:
|
|||||||
QSharedPointer<PageController> pageController() const;
|
QSharedPointer<PageController> pageController() const;
|
||||||
void setQmlRoot();
|
void setQmlRoot();
|
||||||
|
|
||||||
|
void openConnectionByIndex(int serverIndex);
|
||||||
|
void importConfigFromData(const QString &data);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void translationsUpdated();
|
void translationsUpdated();
|
||||||
void websiteUrlChanged(const QString &newUrl);
|
void websiteUrlChanged(const QString &newUrl);
|
||||||
@@ -88,8 +94,6 @@ private:
|
|||||||
void initAutoConnectHandler();
|
void initAutoConnectHandler();
|
||||||
void initAmneziaDnsToggledHandler();
|
void initAmneziaDnsToggledHandler();
|
||||||
void initPrepareConfigHandler();
|
void initPrepareConfigHandler();
|
||||||
void initImportPremiumV2VpnKeyHandler();
|
|
||||||
void initShowMigrationDrawerHandler();
|
|
||||||
void initStrictKillSwitchHandler();
|
void initStrictKillSwitchHandler();
|
||||||
|
|
||||||
QQmlApplicationEngine *m_engine {}; // TODO use parent child system here?
|
QQmlApplicationEngine *m_engine {}; // TODO use parent child system here?
|
||||||
@@ -117,7 +121,7 @@ private:
|
|||||||
|
|
||||||
QScopedPointer<ApiSettingsController> m_apiSettingsController;
|
QScopedPointer<ApiSettingsController> m_apiSettingsController;
|
||||||
QScopedPointer<ApiConfigsController> m_apiConfigsController;
|
QScopedPointer<ApiConfigsController> m_apiConfigsController;
|
||||||
QScopedPointer<ApiPremV1MigrationController> m_apiPremV1MigrationController;
|
QScopedPointer<ApiNewsController> m_apiNewsController;
|
||||||
|
|
||||||
QSharedPointer<ContainersModel> m_containersModel;
|
QSharedPointer<ContainersModel> m_containersModel;
|
||||||
QSharedPointer<ContainersModel> m_defaultServerContainersModel;
|
QSharedPointer<ContainersModel> m_defaultServerContainersModel;
|
||||||
@@ -125,11 +129,14 @@ private:
|
|||||||
QSharedPointer<LanguageModel> m_languageModel;
|
QSharedPointer<LanguageModel> m_languageModel;
|
||||||
QSharedPointer<ProtocolsModel> m_protocolsModel;
|
QSharedPointer<ProtocolsModel> m_protocolsModel;
|
||||||
QSharedPointer<SitesModel> m_sitesModel;
|
QSharedPointer<SitesModel> m_sitesModel;
|
||||||
|
QSharedPointer<NewsModel> m_newsModel;
|
||||||
QSharedPointer<AllowedDnsModel> m_allowedDnsModel;
|
QSharedPointer<AllowedDnsModel> m_allowedDnsModel;
|
||||||
QSharedPointer<AppSplitTunnelingModel> m_appSplitTunnelingModel;
|
QSharedPointer<AppSplitTunnelingModel> m_appSplitTunnelingModel;
|
||||||
QSharedPointer<ClientManagementModel> m_clientManagementModel;
|
QSharedPointer<ClientManagementModel> m_clientManagementModel;
|
||||||
|
|
||||||
QSharedPointer<ApiServicesModel> m_apiServicesModel;
|
QSharedPointer<ApiServicesModel> m_apiServicesModel;
|
||||||
|
QSharedPointer<ApiSubscriptionPlansModel> m_apiSubscriptionPlansModel;
|
||||||
|
QSharedPointer<ApiBenefitsModel> m_apiBenefitsModel;
|
||||||
QSharedPointer<ApiCountryModel> m_apiCountryModel;
|
QSharedPointer<ApiCountryModel> m_apiCountryModel;
|
||||||
QSharedPointer<ApiAccountInfoModel> m_apiAccountInfoModel;
|
QSharedPointer<ApiAccountInfoModel> m_apiAccountInfoModel;
|
||||||
QSharedPointer<ApiDevicesModel> m_apiDevicesModel;
|
QSharedPointer<ApiDevicesModel> m_apiDevicesModel;
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
#include "gatewayController.h"
|
#include "gatewayController.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <functional>
|
||||||
#include <random>
|
#include <random>
|
||||||
|
|
||||||
|
#include <QCryptographicHash>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
|
#include <QPromise>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
|
||||||
#include "QBlockCipher.h"
|
#include "QBlockCipher.h"
|
||||||
@@ -38,6 +41,16 @@ namespace
|
|||||||
constexpr QLatin1String errorResponsePattern3("Account not found.");
|
constexpr QLatin1String errorResponsePattern3("Account not found.");
|
||||||
|
|
||||||
constexpr QLatin1String updateRequestResponsePattern("client version update is required");
|
constexpr QLatin1String updateRequestResponsePattern("client version update is required");
|
||||||
|
|
||||||
|
constexpr int httpStatusCodeNotFound = 404;
|
||||||
|
constexpr int httpStatusCodeConflict = 409;
|
||||||
|
constexpr int httpStatusCodeNotImplemented = 501;
|
||||||
|
constexpr int httpStatusCodePaymentRequired = 402;
|
||||||
|
constexpr int httpStatusCodeUnprocessableEntity = 422;
|
||||||
|
|
||||||
|
constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?");
|
||||||
|
|
||||||
|
constexpr int proxyStorageRequestTimeoutMsecs = 3000;
|
||||||
}
|
}
|
||||||
|
|
||||||
GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
|
GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
|
||||||
@@ -50,103 +63,45 @@ GatewayController::GatewayController(const QString &gatewayEndpoint, const bool
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
ErrorCode GatewayController::get(const QString &endpoint, QByteArray &responseBody)
|
GatewayController::EncryptedRequestData GatewayController::prepareRequest(const QString &endpoint, const QJsonObject &apiPayload)
|
||||||
{
|
{
|
||||||
|
EncryptedRequestData encRequestData;
|
||||||
|
encRequestData.errorCode = ErrorCode::NoError;
|
||||||
|
|
||||||
#ifdef Q_OS_IOS
|
#ifdef Q_OS_IOS
|
||||||
IosController::Instance()->requestInetAccess();
|
IosController::Instance()->requestInetAccess();
|
||||||
QThread::msleep(10);
|
QThread::msleep(10);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
QNetworkRequest request;
|
encRequestData.request.setTransferTimeout(m_requestTimeoutMsecs);
|
||||||
request.setTransferTimeout(m_requestTimeoutMsecs);
|
encRequestData.request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
encRequestData.request.setRawHeader(QString("X-Client-Request-ID").toUtf8(), QUuid::createUuid().toString(QUuid::WithoutBraces).toUtf8());
|
||||||
request.setRawHeader(QString("X-Client-Request-ID").toUtf8(), QUuid::createUuid().toString(QUuid::WithoutBraces).toUtf8());
|
encRequestData.request.setUrl(endpoint.arg(m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl));
|
||||||
|
|
||||||
request.setUrl(QString(endpoint).arg(m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl));
|
|
||||||
|
|
||||||
// bypass killSwitch exceptions for API-gateway
|
// bypass killSwitch exceptions for API-gateway
|
||||||
#ifdef AMNEZIA_DESKTOP
|
#ifdef AMNEZIA_DESKTOP
|
||||||
if (m_isStrictKillSwitchEnabled) {
|
if (m_isStrictKillSwitchEnabled) {
|
||||||
QString host = QUrl(request.url()).host();
|
QString host = QUrl(encRequestData.request.url()).host();
|
||||||
QString ip = NetworkUtilities::getIPAddress(host);
|
QString ip = NetworkUtilities::getIPAddress(host);
|
||||||
if (!ip.isEmpty()) {
|
if (!ip.isEmpty()) {
|
||||||
IpcClient::Interface()->addKillSwitchAllowedRange(QStringList { ip });
|
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||||
}
|
QRemoteObjectPendingReply<bool> reply = iface->addKillSwitchAllowedRange(QStringList { ip });
|
||||||
}
|
if (!reply.waitForFinished(1000) || !reply.returnValue())
|
||||||
#endif
|
qWarning() << "GatewayController::prepareRequest(): Failed to execute remote addKillSwitchAllowedRange call";
|
||||||
|
});
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
|
|
||||||
{
|
|
||||||
#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(QString("X-Client-Request-ID").toUtf8(), QUuid::createUuid().toString(QUuid::WithoutBraces).toUtf8());
|
|
||||||
|
|
||||||
request.setUrl(endpoint.arg(m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl));
|
|
||||||
|
|
||||||
// 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 });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
QSimpleCrypto::QBlockCipher blockCipher;
|
QSimpleCrypto::QBlockCipher blockCipher;
|
||||||
QByteArray key = blockCipher.generatePrivateSalt(32);
|
encRequestData.key = blockCipher.generatePrivateSalt(32);
|
||||||
QByteArray iv = blockCipher.generatePrivateSalt(32);
|
encRequestData.iv = blockCipher.generatePrivateSalt(32);
|
||||||
QByteArray salt = blockCipher.generatePrivateSalt(8);
|
encRequestData.salt = blockCipher.generatePrivateSalt(8);
|
||||||
|
|
||||||
QJsonObject keyPayload;
|
QJsonObject keyPayload;
|
||||||
keyPayload[configKey::aesKey] = QString(key.toBase64());
|
keyPayload[configKey::aesKey] = QString(encRequestData.key.toBase64());
|
||||||
keyPayload[configKey::aesIv] = QString(iv.toBase64());
|
keyPayload[configKey::aesIv] = QString(encRequestData.iv.toBase64());
|
||||||
keyPayload[configKey::aesSalt] = QString(salt.toBase64());
|
keyPayload[configKey::aesSalt] = QString(encRequestData.salt.toBase64());
|
||||||
|
|
||||||
QByteArray encryptedKeyPayload;
|
QByteArray encryptedKeyPayload;
|
||||||
QByteArray encryptedApiPayload;
|
QByteArray encryptedApiPayload;
|
||||||
@@ -161,96 +116,277 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
|
|||||||
} catch (...) {
|
} catch (...) {
|
||||||
Utils::logException();
|
Utils::logException();
|
||||||
qCritical() << "error loading public key from environment variables";
|
qCritical() << "error loading public key from environment variables";
|
||||||
return ErrorCode::ApiMissingAgwPublicKey;
|
encRequestData.errorCode = ErrorCode::ApiMissingAgwPublicKey;
|
||||||
|
return encRequestData;
|
||||||
}
|
}
|
||||||
|
|
||||||
encryptedKeyPayload = rsa.encrypt(QJsonDocument(keyPayload).toJson(), publicKey, RSA_PKCS1_PADDING);
|
encryptedKeyPayload = rsa.encrypt(QJsonDocument(keyPayload).toJson(), publicKey, RSA_PKCS1_PADDING);
|
||||||
EVP_PKEY_free(publicKey);
|
EVP_PKEY_free(publicKey);
|
||||||
|
|
||||||
encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(), key, iv, "", salt);
|
encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(), encRequestData.key, encRequestData.iv,
|
||||||
} catch (...) { // todo change error handling in QSimpleCrypto?
|
"", encRequestData.salt);
|
||||||
|
} catch (...) {
|
||||||
Utils::logException();
|
Utils::logException();
|
||||||
qCritical() << "error when encrypting the request body";
|
qCritical() << "error when encrypting the request body";
|
||||||
return ErrorCode::ApiConfigDecryptionError;
|
encRequestData.errorCode = ErrorCode::ApiConfigDecryptionError;
|
||||||
|
return encRequestData;
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject requestBody;
|
QJsonObject requestBody;
|
||||||
requestBody[configKey::keyPayload] = QString(encryptedKeyPayload.toBase64());
|
requestBody[configKey::keyPayload] = QString(encryptedKeyPayload.toBase64());
|
||||||
requestBody[configKey::apiPayload] = QString(encryptedApiPayload.toBase64());
|
requestBody[configKey::apiPayload] = QString(encryptedApiPayload.toBase64());
|
||||||
|
|
||||||
QNetworkReply *reply = amnApp->networkManager()->post(request, QJsonDocument(requestBody).toJson());
|
encRequestData.requestBody = QJsonDocument(requestBody).toJson();
|
||||||
|
return encRequestData;
|
||||||
|
}
|
||||||
|
|
||||||
|
GatewayController::DecryptionResult GatewayController::tryDecryptResponseBody(const QByteArray &encryptedResponseBody,
|
||||||
|
QNetworkReply::NetworkError replyError, const QByteArray &key,
|
||||||
|
const QByteArray &iv, const QByteArray &salt)
|
||||||
|
{
|
||||||
|
DecryptionResult result;
|
||||||
|
result.decryptedBody = encryptedResponseBody;
|
||||||
|
result.isDecryptionSuccessful = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
QSimpleCrypto::QBlockCipher blockCipher;
|
||||||
|
result.decryptedBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt);
|
||||||
|
result.isDecryptionSuccessful = true;
|
||||||
|
} catch (...) {
|
||||||
|
result.decryptedBody = encryptedResponseBody;
|
||||||
|
result.isDecryptionSuccessful = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
|
||||||
|
{
|
||||||
|
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload);
|
||||||
|
if (encRequestData.errorCode != ErrorCode::NoError) {
|
||||||
|
return encRequestData.errorCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
QNetworkReply *reply = amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
|
||||||
|
|
||||||
QEventLoop wait;
|
QEventLoop wait;
|
||||||
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
||||||
|
|
||||||
QList<QSslError> sslErrors;
|
QList<QSslError> sslErrors;
|
||||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||||
wait.exec();
|
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||||
|
|
||||||
QByteArray encryptedResponseBody = reply->readAll();
|
QByteArray encryptedResponseBody = reply->readAll();
|
||||||
|
QString replyErrorString = reply->errorString();
|
||||||
|
auto replyError = reply->error();
|
||||||
|
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
|
|
||||||
if (sslErrors.isEmpty() && shouldBypassProxy(reply, encryptedResponseBody, true, key, iv, salt)) {
|
reply->deleteLater();
|
||||||
auto requestFunction = [&request, &encryptedResponseBody, &requestBody](const QString &url) {
|
|
||||||
request.setUrl(url);
|
auto decryptionResult =
|
||||||
return amnApp->networkManager()->post(request, QJsonDocument(requestBody).toJson());
|
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||||
|
|
||||||
|
if (sslErrors.isEmpty() && shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
|
||||||
|
auto requestFunction = [&encRequestData, &encryptedResponseBody](const QString &url) {
|
||||||
|
encRequestData.request.setUrl(url);
|
||||||
|
return amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
|
||||||
};
|
};
|
||||||
|
|
||||||
auto replyProcessingFunction = [&encryptedResponseBody, &reply, &sslErrors, &key, &iv, &salt,
|
auto replyProcessingFunction = [&encryptedResponseBody, &replyErrorString, &replyError, &httpStatusCode, &sslErrors, &encRequestData,
|
||||||
this](QNetworkReply *nestedReply, const QList<QSslError> &nestedSslErrors) {
|
&decryptionResult, this](QNetworkReply *reply, const QList<QSslError> &nestedSslErrors) {
|
||||||
encryptedResponseBody = nestedReply->readAll();
|
encryptedResponseBody = reply->readAll();
|
||||||
reply = nestedReply;
|
replyErrorString = reply->errorString();
|
||||||
if (!sslErrors.isEmpty() || shouldBypassProxy(nestedReply, encryptedResponseBody, true, key, iv, salt)) {
|
replyError = reply->error();
|
||||||
|
httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
|
|
||||||
|
decryptionResult =
|
||||||
|
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||||
|
|
||||||
|
if (!sslErrors.isEmpty()
|
||||||
|
|| shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
|
||||||
sslErrors = nestedSslErrors;
|
sslErrors = nestedSslErrors;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
bypassProxy(endpoint, reply, requestFunction, replyProcessingFunction);
|
auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
|
||||||
|
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString("");
|
||||||
|
bypassProxy(endpoint, serviceType, userCountryCode, requestFunction, replyProcessingFunction);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, reply);
|
auto errorCode =
|
||||||
reply->deleteLater();
|
apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, decryptionResult.decryptedBody);
|
||||||
if (errorCode) {
|
if (errorCode) {
|
||||||
return errorCode;
|
return errorCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (!decryptionResult.isDecryptionSuccessful) {
|
||||||
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";
|
qCritical() << "error when decrypting the request body";
|
||||||
return ErrorCode::ApiConfigDecryptionError;
|
return ErrorCode::ApiConfigDecryptionError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
responseBody = decryptionResult.decryptedBody;
|
||||||
|
return ErrorCode::NoError;
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList GatewayController::getProxyUrls()
|
QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload)
|
||||||
|
{
|
||||||
|
auto promise = QSharedPointer<QPromise<QPair<ErrorCode, QByteArray>>>::create();
|
||||||
|
promise->start();
|
||||||
|
|
||||||
|
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload);
|
||||||
|
if (encRequestData.errorCode != ErrorCode::NoError) {
|
||||||
|
promise->addResult(qMakePair(encRequestData.errorCode, QByteArray()));
|
||||||
|
promise->finish();
|
||||||
|
return promise->future();
|
||||||
|
}
|
||||||
|
|
||||||
|
QNetworkReply *reply = amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
|
||||||
|
|
||||||
|
auto sslErrors = QSharedPointer<QList<QSslError>>::create();
|
||||||
|
|
||||||
|
connect(reply, &QNetworkReply::sslErrors, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
|
||||||
|
|
||||||
|
connect(reply, &QNetworkReply::finished, reply, [promise, sslErrors, encRequestData, endpoint, apiPayload, reply, this]() mutable {
|
||||||
|
QByteArray encryptedResponseBody = reply->readAll();
|
||||||
|
QString replyErrorString = reply->errorString();
|
||||||
|
auto replyError = reply->error();
|
||||||
|
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
|
|
||||||
|
reply->deleteLater();
|
||||||
|
|
||||||
|
auto decryptionResult =
|
||||||
|
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||||
|
|
||||||
|
auto processResponse = [promise, encRequestData](const GatewayController::DecryptionResult &decryptionResult,
|
||||||
|
const QList<QSslError> &sslErrors, QNetworkReply::NetworkError replyError,
|
||||||
|
const QString &replyErrorString, int httpStatusCode) {
|
||||||
|
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode,
|
||||||
|
decryptionResult.decryptedBody);
|
||||||
|
if (errorCode) {
|
||||||
|
promise->addResult(qMakePair(errorCode, QByteArray()));
|
||||||
|
promise->finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!decryptionResult.isDecryptionSuccessful) {
|
||||||
|
Utils::logException();
|
||||||
|
qCritical() << "error when decrypting the request body";
|
||||||
|
promise->addResult(qMakePair(ErrorCode::ApiConfigDecryptionError, QByteArray()));
|
||||||
|
promise->finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
promise->addResult(qMakePair(ErrorCode::NoError, decryptionResult.decryptedBody));
|
||||||
|
promise->finish();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sslErrors->isEmpty() && shouldBypassProxy(replyError, decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful)) {
|
||||||
|
auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
|
||||||
|
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString("");
|
||||||
|
|
||||||
|
QStringList primaryBaseUrls;
|
||||||
|
QStringList fallbackBaseUrls;
|
||||||
|
if (m_isDevEnvironment) {
|
||||||
|
primaryBaseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||||
|
} else {
|
||||||
|
primaryBaseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||||
|
fallbackBaseUrls = QString(FALLBACK_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto appendStorageUrls = [&serviceType, &userCountryCode](const QStringList &baseUrls, QStringList &target) {
|
||||||
|
if (!serviceType.isEmpty()) {
|
||||||
|
for (const auto &baseUrl : baseUrls) {
|
||||||
|
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
|
||||||
|
target.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto &baseUrl : baseUrls) {
|
||||||
|
target.push_back(baseUrl + "endpoints.json");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
QStringList proxyStorageUrls;
|
||||||
|
appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
|
||||||
|
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
|
||||||
|
|
||||||
|
getProxyUrlsAsync(proxyStorageUrls, 0, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
|
||||||
|
getProxyUrlAsync(proxyUrls, 0, [this, encRequestData, endpoint, processResponse](const QString &proxyUrl) {
|
||||||
|
bypassProxyAsync(endpoint, proxyUrl, encRequestData,
|
||||||
|
[processResponse, this](const QByteArray &decryptedBody, bool isDecryptionSuccessful,
|
||||||
|
const QList<QSslError> &sslErrors, QNetworkReply::NetworkError replyError,
|
||||||
|
const QString &replyErrorString, int httpStatusCode) {
|
||||||
|
GatewayController::DecryptionResult result;
|
||||||
|
result.decryptedBody = decryptedBody;
|
||||||
|
result.isDecryptionSuccessful = isDecryptionSuccessful;
|
||||||
|
processResponse(result, sslErrors, replyError, replyErrorString, httpStatusCode);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
processResponse(decryptionResult, *sslErrors, replyError, replyErrorString, httpStatusCode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise->future();
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList GatewayController::getProxyUrls(const QString &serviceType, const QString &userCountryCode)
|
||||||
{
|
{
|
||||||
QNetworkRequest request;
|
QNetworkRequest request;
|
||||||
request.setTransferTimeout(m_requestTimeoutMsecs);
|
request.setTransferTimeout(proxyStorageRequestTimeoutMsecs);
|
||||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
|
||||||
QEventLoop wait;
|
QEventLoop wait;
|
||||||
QList<QSslError> sslErrors;
|
QList<QSslError> sslErrors;
|
||||||
QNetworkReply *reply;
|
QNetworkReply *reply;
|
||||||
|
|
||||||
QStringList proxyStorageUrls;
|
QStringList primaryBaseUrls;
|
||||||
|
QStringList fallbackBaseUrls;
|
||||||
if (m_isDevEnvironment) {
|
if (m_isDevEnvironment) {
|
||||||
proxyStorageUrls = QString(DEV_S3_ENDPOINT).split(", ");
|
primaryBaseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||||
} else {
|
} else {
|
||||||
proxyStorageUrls = QString(PROD_S3_ENDPOINT).split(", ");
|
primaryBaseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||||
|
fallbackBaseUrls = QString(FALLBACK_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::random_device randomDevice;
|
||||||
|
std::mt19937 generator(randomDevice());
|
||||||
|
std::shuffle(primaryBaseUrls.begin(), primaryBaseUrls.end(), generator);
|
||||||
|
std::shuffle(fallbackBaseUrls.begin(), fallbackBaseUrls.end(), generator);
|
||||||
|
|
||||||
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||||
|
|
||||||
|
auto appendStorageUrls = [&serviceType, &userCountryCode](const QStringList &baseUrls, QStringList &target) {
|
||||||
|
if (!serviceType.isEmpty()) {
|
||||||
|
for (const auto &baseUrl : baseUrls) {
|
||||||
|
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
|
||||||
|
target.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto &baseUrl : baseUrls) {
|
||||||
|
target.push_back(baseUrl + "endpoints.json");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
QStringList proxyStorageUrls;
|
||||||
|
appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
|
||||||
|
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
|
||||||
|
|
||||||
|
if (proxyStorageUrls.empty()) {
|
||||||
|
qDebug() << "empty storage endpoint list";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
for (const auto &proxyStorageUrl : proxyStorageUrls) {
|
for (const auto &proxyStorageUrl : proxyStorageUrls) {
|
||||||
request.setUrl(proxyStorageUrl);
|
request.setUrl(proxyStorageUrl);
|
||||||
reply = amnApp->networkManager()->get(request);
|
reply = amnApp->networkManager()->get(request);
|
||||||
|
|
||||||
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
||||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||||
wait.exec();
|
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||||
|
|
||||||
if (reply->error() == QNetworkReply::NetworkError::NoError) {
|
if (reply->error() == QNetworkReply::NetworkError::NoError) {
|
||||||
auto encryptedResponseBody = reply->readAll();
|
auto encryptedResponseBody = reply->readAll();
|
||||||
@@ -288,7 +424,10 @@ QStringList GatewayController::getProxyUrls()
|
|||||||
}
|
}
|
||||||
return endpoints;
|
return endpoints;
|
||||||
} else {
|
} else {
|
||||||
apiUtils::checkNetworkReplyErrors(sslErrors, reply);
|
auto replyError = reply->error();
|
||||||
|
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
|
qDebug() << replyError;
|
||||||
|
qDebug() << httpStatusCode;
|
||||||
qDebug() << "go to the next storage endpoint";
|
qDebug() << "go to the next storage endpoint";
|
||||||
|
|
||||||
reply->deleteLater();
|
reply->deleteLater();
|
||||||
@@ -297,75 +436,94 @@ QStringList GatewayController::getProxyUrls()
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
bool GatewayController::shouldBypassProxy(QNetworkReply *reply, const QByteArray &responseBody, bool checkEncryption, const QByteArray &key,
|
bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody,
|
||||||
const QByteArray &iv, const QByteArray &salt)
|
bool isDecryptionSuccessful)
|
||||||
{
|
{
|
||||||
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError || reply->error() == QNetworkReply::NetworkError::TimeoutError) {
|
const QByteArray &responseBody = decryptedResponseBody;
|
||||||
qDebug() << "timeout occurred";
|
|
||||||
qDebug() << reply->error();
|
int apiHttpStatus = -1;
|
||||||
|
QString apiErrorMessage;
|
||||||
|
if (isDecryptionSuccessful) {
|
||||||
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
||||||
|
if (jsonDoc.isObject()) {
|
||||||
|
QJsonObject jsonObj = jsonDoc.object();
|
||||||
|
apiHttpStatus = jsonObj.value("http_status").toInt(-1);
|
||||||
|
apiErrorMessage = jsonObj.value(QStringLiteral("message")).toString().trimmed();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
qDebug() << "failed to decrypt the data";
|
||||||
return true;
|
return true;
|
||||||
} else if (responseBody.contains("html")) {
|
}
|
||||||
|
|
||||||
|
if (replyError == QNetworkReply::NetworkError::OperationCanceledError || replyError == QNetworkReply::NetworkError::TimeoutError) {
|
||||||
|
qDebug() << "timeout occurred";
|
||||||
|
qDebug() << replyError;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (responseBody.contains("html")) {
|
||||||
qDebug() << "the response contains an html tag";
|
qDebug() << "the response contains an html tag";
|
||||||
return true;
|
return true;
|
||||||
} else if (reply->error() == QNetworkReply::NetworkError::ContentNotFoundError) {
|
}
|
||||||
|
if (apiHttpStatus == httpStatusCodeNotFound) {
|
||||||
if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2)
|
if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2)
|
||||||
|| responseBody.contains(errorResponsePattern3)) {
|
|| responseBody.contains(errorResponsePattern3)) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
qDebug() << reply->error();
|
qDebug() << replyError;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} else if (reply->error() == QNetworkReply::NetworkError::OperationNotImplementedError) {
|
}
|
||||||
|
if (apiHttpStatus == httpStatusCodeNotImplemented) {
|
||||||
if (responseBody.contains(updateRequestResponsePattern)) {
|
if (responseBody.contains(updateRequestResponsePattern)) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
qDebug() << reply->error();
|
qDebug() << replyError;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} else if (reply->error() != QNetworkReply::NetworkError::NoError) {
|
}
|
||||||
qDebug() << reply->error();
|
if (apiHttpStatus == httpStatusCodeConflict) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (apiHttpStatus == httpStatusCodePaymentRequired) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (apiHttpStatus == httpStatusCodeUnprocessableEntity) {
|
||||||
|
return apiErrorMessage != unprocessableSubscriptionMessage;
|
||||||
|
}
|
||||||
|
if (replyError != QNetworkReply::NetworkError::NoError) {
|
||||||
|
qDebug() << replyError;
|
||||||
return true;
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void GatewayController::bypassProxy(const QString &endpoint, QNetworkReply *reply,
|
void GatewayController::bypassProxy(const QString &endpoint, const QString &serviceType, const QString &userCountryCode,
|
||||||
std::function<QNetworkReply *(const QString &url)> requestFunction,
|
std::function<QNetworkReply *(const QString &url)> requestFunction,
|
||||||
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction)
|
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction)
|
||||||
{
|
{
|
||||||
QStringList proxyUrls = getProxyUrls();
|
QStringList proxyUrls = getProxyUrls(serviceType, userCountryCode);
|
||||||
std::random_device randomDevice;
|
std::random_device randomDevice;
|
||||||
std::mt19937 generator(randomDevice());
|
std::mt19937 generator(randomDevice());
|
||||||
std::shuffle(proxyUrls.begin(), proxyUrls.end(), generator);
|
std::shuffle(proxyUrls.begin(), proxyUrls.end(), generator);
|
||||||
|
|
||||||
QByteArray responseBody;
|
QByteArray responseBody;
|
||||||
|
|
||||||
auto bypassFunction = [this](const QString &endpoint, const QString &proxyUrl, QNetworkReply *reply,
|
auto bypassFunction = [this](const QString &endpoint, const QString &proxyUrl,
|
||||||
std::function<QNetworkReply *(const QString &url)> requestFunction,
|
std::function<QNetworkReply *(const QString &url)> requestFunction,
|
||||||
std::function<bool(QNetworkReply * reply, const QList<QSslError> &sslErrors)> replyProcessingFunction) {
|
std::function<bool(QNetworkReply * reply, const QList<QSslError> &sslErrors)> replyProcessingFunction) {
|
||||||
QEventLoop wait;
|
QEventLoop wait;
|
||||||
QList<QSslError> sslErrors;
|
QList<QSslError> sslErrors;
|
||||||
|
|
||||||
qDebug() << "go to the next proxy endpoint";
|
qDebug() << "go to the next proxy endpoint";
|
||||||
reply->deleteLater(); // delete the previous reply
|
QNetworkReply *reply = requestFunction(endpoint.arg(proxyUrl));
|
||||||
reply = requestFunction(endpoint.arg(proxyUrl));
|
|
||||||
|
|
||||||
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
||||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||||
wait.exec();
|
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||||
|
|
||||||
if (replyProcessingFunction(reply, sslErrors)) {
|
auto result = replyProcessingFunction(reply, sslErrors);
|
||||||
return true;
|
reply->deleteLater();
|
||||||
}
|
return result;
|
||||||
return false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (m_proxyUrl.isEmpty()) {
|
if (m_proxyUrl.isEmpty()) {
|
||||||
@@ -383,7 +541,7 @@ void GatewayController::bypassProxy(const QString &endpoint, QNetworkReply *repl
|
|||||||
|
|
||||||
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
||||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||||
wait.exec();
|
wait.exec(QEventLoop::ExcludeUserInputEvents);
|
||||||
|
|
||||||
if (reply->error() == QNetworkReply::NetworkError::NoError) {
|
if (reply->error() == QNetworkReply::NetworkError::NoError) {
|
||||||
reply->deleteLater();
|
reply->deleteLater();
|
||||||
@@ -399,15 +557,151 @@ void GatewayController::bypassProxy(const QString &endpoint, QNetworkReply *repl
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!m_proxyUrl.isEmpty()) {
|
if (!m_proxyUrl.isEmpty()) {
|
||||||
if (bypassFunction(endpoint, m_proxyUrl, reply, requestFunction, replyProcessingFunction)) {
|
if (bypassFunction(endpoint, m_proxyUrl, requestFunction, replyProcessingFunction)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const QString &proxyUrl : proxyUrls) {
|
for (const QString &proxyUrl : proxyUrls) {
|
||||||
if (bypassFunction(endpoint, proxyUrl, reply, requestFunction, replyProcessingFunction)) {
|
if (bypassFunction(endpoint, proxyUrl, requestFunction, replyProcessingFunction)) {
|
||||||
m_proxyUrl = proxyUrl;
|
m_proxyUrl = proxyUrl;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
|
||||||
|
std::function<void(const QStringList &)> onComplete)
|
||||||
|
{
|
||||||
|
if (currentProxyStorageIndex >= proxyStorageUrls.size()) {
|
||||||
|
onComplete({});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QNetworkRequest request;
|
||||||
|
request.setTransferTimeout(proxyStorageRequestTimeoutMsecs);
|
||||||
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
request.setUrl(proxyStorageUrls[currentProxyStorageIndex]);
|
||||||
|
|
||||||
|
QNetworkReply *reply = amnApp->networkManager()->get(request);
|
||||||
|
|
||||||
|
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) { *(state->sslErrors) = e; });
|
||||||
|
|
||||||
|
connect(reply, &QNetworkReply::finished, this, [this, proxyStorageUrls, currentProxyStorageIndex, onComplete, reply]() {
|
||||||
|
if (reply->error() == QNetworkReply::NoError) {
|
||||||
|
QByteArray encrypted = reply->readAll();
|
||||||
|
reply->deleteLater();
|
||||||
|
|
||||||
|
QByteArray responseBody;
|
||||||
|
try {
|
||||||
|
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||||
|
if (!m_isDevEnvironment) {
|
||||||
|
QCryptographicHash hash(QCryptographicHash::Sha512);
|
||||||
|
hash.addData(key);
|
||||||
|
QByteArray h = hash.result().toHex();
|
||||||
|
|
||||||
|
QByteArray decKey = QByteArray::fromHex(h.left(64));
|
||||||
|
QByteArray iv = QByteArray::fromHex(h.mid(64, 32));
|
||||||
|
QByteArray ba = QByteArray::fromBase64(encrypted);
|
||||||
|
|
||||||
|
QSimpleCrypto::QBlockCipher cipher;
|
||||||
|
responseBody = cipher.decryptAesBlockCipher(ba, decKey, iv);
|
||||||
|
} else {
|
||||||
|
responseBody = encrypted;
|
||||||
|
}
|
||||||
|
} catch (...) {
|
||||||
|
Utils::logException();
|
||||||
|
qCritical() << "error decrypting payload";
|
||||||
|
QMetaObject::invokeMethod(
|
||||||
|
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonArray endpointsArray = QJsonDocument::fromJson(responseBody).array();
|
||||||
|
QStringList endpoints;
|
||||||
|
for (const QJsonValue &endpoint : endpointsArray)
|
||||||
|
endpoints.push_back(endpoint.toString());
|
||||||
|
|
||||||
|
QStringList shuffled = endpoints;
|
||||||
|
std::random_device randomDevice;
|
||||||
|
std::mt19937 generator(randomDevice());
|
||||||
|
std::shuffle(shuffled.begin(), shuffled.end(), generator);
|
||||||
|
|
||||||
|
onComplete(shuffled);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
|
qDebug() << httpStatusCode;
|
||||||
|
qDebug() << "go to the next storage endpoint";
|
||||||
|
reply->deleteLater();
|
||||||
|
QMetaObject::invokeMethod(
|
||||||
|
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void GatewayController::getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex,
|
||||||
|
std::function<void(const QString &)> onComplete)
|
||||||
|
{
|
||||||
|
if (currentProxyIndex >= proxyUrls.size()) {
|
||||||
|
onComplete("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QNetworkRequest request;
|
||||||
|
request.setTransferTimeout(1000);
|
||||||
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
request.setUrl(proxyUrls[currentProxyIndex] + "lmbd-health");
|
||||||
|
|
||||||
|
QNetworkReply *reply = amnApp->networkManager()->get(request);
|
||||||
|
|
||||||
|
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) {
|
||||||
|
// *(state->sslErrors) = e;
|
||||||
|
// });
|
||||||
|
|
||||||
|
connect(reply, &QNetworkReply::finished, this, [this, proxyUrls, currentProxyIndex, onComplete, reply]() {
|
||||||
|
reply->deleteLater();
|
||||||
|
|
||||||
|
if (reply->error() == QNetworkReply::NoError) {
|
||||||
|
m_proxyUrl = proxyUrls[currentProxyIndex];
|
||||||
|
onComplete(m_proxyUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "go to the next proxy endpoint";
|
||||||
|
QMetaObject::invokeMethod(this, [=]() { getProxyUrlAsync(proxyUrls, currentProxyIndex + 1, onComplete); }, Qt::QueuedConnection);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void GatewayController::bypassProxyAsync(
|
||||||
|
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
|
||||||
|
std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete)
|
||||||
|
{
|
||||||
|
auto sslErrors = QSharedPointer<QList<QSslError>>::create();
|
||||||
|
if (proxyUrl.isEmpty()) {
|
||||||
|
onComplete(QByteArray(), false, *sslErrors, QNetworkReply::InternalServerError, "empty proxy url", 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QNetworkRequest request = encRequestData.request;
|
||||||
|
request.setUrl(endpoint.arg(proxyUrl));
|
||||||
|
|
||||||
|
QNetworkReply *reply = amnApp->networkManager()->post(request, encRequestData.requestBody);
|
||||||
|
|
||||||
|
connect(reply, &QNetworkReply::sslErrors, this, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
|
||||||
|
|
||||||
|
connect(reply, &QNetworkReply::finished, this, [sslErrors, onComplete, encRequestData, reply, this]() {
|
||||||
|
QByteArray encryptedResponseBody = reply->readAll();
|
||||||
|
QString replyErrorString = reply->errorString();
|
||||||
|
auto replyError = reply->error();
|
||||||
|
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
|
|
||||||
|
reply->deleteLater();
|
||||||
|
|
||||||
|
auto decryptionResult =
|
||||||
|
tryDecryptResponseBody(encryptedResponseBody, replyError, encRequestData.key, encRequestData.iv, encRequestData.salt);
|
||||||
|
|
||||||
|
onComplete(decryptionResult.decryptedBody, decryptionResult.isDecryptionSuccessful, *sslErrors, replyError, replyErrorString,
|
||||||
|
httpStatusCode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
#ifndef GATEWAYCONTROLLER_H
|
#ifndef GATEWAYCONTROLLER_H
|
||||||
#define GATEWAYCONTROLLER_H
|
#define GATEWAYCONTROLLER_H
|
||||||
|
|
||||||
|
#include <QFuture>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <QPair>
|
||||||
|
#include <QPromise>
|
||||||
|
#include <QSharedPointer>
|
||||||
|
|
||||||
#include "core/defs.h"
|
#include "core/defs.h"
|
||||||
|
|
||||||
@@ -18,16 +22,43 @@ public:
|
|||||||
explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
|
explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
|
||||||
const bool isStrictKillSwitchEnabled, QObject *parent = nullptr);
|
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);
|
amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody);
|
||||||
|
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject apiPayload);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QStringList getProxyUrls();
|
struct EncryptedRequestData
|
||||||
bool shouldBypassProxy(QNetworkReply *reply, const QByteArray &responseBody, bool checkEncryption, const QByteArray &key = "",
|
{
|
||||||
const QByteArray &iv = "", const QByteArray &salt = "");
|
QNetworkRequest request;
|
||||||
void bypassProxy(const QString &endpoint, QNetworkReply *reply, std::function<QNetworkReply *(const QString &url)> requestFunction,
|
QByteArray requestBody;
|
||||||
|
QByteArray key;
|
||||||
|
QByteArray iv;
|
||||||
|
QByteArray salt;
|
||||||
|
amnezia::ErrorCode errorCode;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DecryptionResult
|
||||||
|
{
|
||||||
|
QByteArray decryptedBody;
|
||||||
|
bool isDecryptionSuccessful;
|
||||||
|
};
|
||||||
|
|
||||||
|
EncryptedRequestData prepareRequest(const QString &endpoint, const QJsonObject &apiPayload);
|
||||||
|
DecryptionResult tryDecryptResponseBody(const QByteArray &encryptedResponseBody, QNetworkReply::NetworkError replyError,
|
||||||
|
const QByteArray &key, const QByteArray &iv, const QByteArray &salt);
|
||||||
|
|
||||||
|
QStringList getProxyUrls(const QString &serviceType, const QString &userCountryCode);
|
||||||
|
bool shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody, bool isDecryptionSuccessful);
|
||||||
|
void bypassProxy(const QString &endpoint, const QString &serviceType, const QString &userCountryCode,
|
||||||
|
std::function<QNetworkReply *(const QString &url)> requestFunction,
|
||||||
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction);
|
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction);
|
||||||
|
|
||||||
|
void getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
|
||||||
|
std::function<void(const QStringList &)> onComplete);
|
||||||
|
void getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function<void(const QString &)> onComplete);
|
||||||
|
void bypassProxyAsync(
|
||||||
|
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
|
||||||
|
std::function<void(const QByteArray &, bool, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete);
|
||||||
|
|
||||||
int m_requestTimeoutMsecs;
|
int m_requestTimeoutMsecs;
|
||||||
QString m_gatewayEndpoint;
|
QString m_gatewayEndpoint;
|
||||||
bool m_isDevEnvironment = false;
|
bool m_isDevEnvironment = false;
|
||||||
|
|||||||
@@ -345,7 +345,7 @@ bool ServerController::isReinstallContainerRequired(DockerContainer container, c
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (container == DockerContainer::Awg) {
|
if (ContainerProps::isAwgContainer(container)) {
|
||||||
if ((oldProtoConfig.value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress)
|
if ((oldProtoConfig.value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress)
|
||||||
!= newProtoConfig.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)
|
|| (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)
|
|| (oldProtoConfig.value(config_key::underloadPacketMagicHeader).toString(protocols::awg::defaultUnderloadPacketMagicHeader)
|
||||||
!= newProtoConfig.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))
|
|| (oldProtoConfig.value(config_key::transportPacketMagicHeader).toString(protocols::awg::defaultTransportPacketMagicHeader))
|
||||||
!= newProtoConfig.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)
|
|| (oldProtoConfig.value(config_key::cookieReplyPacketJunkSize).toString(protocols::awg::defaultCookieReplyPacketJunkSize)
|
||||||
// != newProtoConfig.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)
|
|| (oldProtoConfig.value(config_key::transportPacketJunkSize).toString(protocols::awg::defaultTransportPacketJunkSize)
|
||||||
// != newProtoConfig.value(config_key::transportPacketJunkSize).toString(protocols::awg::defaultTransportPacketJunkSize))
|
!= newProtoConfig.value(config_key::transportPacketJunkSize).toString(protocols::awg::defaultTransportPacketJunkSize)))
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -419,6 +419,18 @@ ErrorCode ServerController::installDockerWorker(const ServerCredentials &credent
|
|||||||
cbReadStdOut, cbReadStdErr);
|
cbReadStdOut, cbReadStdErr);
|
||||||
|
|
||||||
qDebug().noquote() << "ServerController::installDockerWorker" << stdOut;
|
qDebug().noquote() << "ServerController::installDockerWorker" << stdOut;
|
||||||
|
if (container == DockerContainer::Awg2) {
|
||||||
|
QRegularExpression regex(R"(Linux\s+(\d+)\.(\d+)[^\d]*)");
|
||||||
|
QRegularExpressionMatch match = regex.match(stdOut);
|
||||||
|
if (match.hasMatch()) {
|
||||||
|
int majorVersion = match.captured(1).toInt();
|
||||||
|
int minorVersion = match.captured(2).toInt();
|
||||||
|
|
||||||
|
if (majorVersion < 4 || (majorVersion == 4 && minorVersion < 14)) {
|
||||||
|
return ErrorCode::ServerLinuxKernelTooOld;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (stdOut.contains("lock"))
|
if (stdOut.contains("lock"))
|
||||||
return ErrorCode::ServerPacketManagerError;
|
return ErrorCode::ServerPacketManagerError;
|
||||||
if (stdOut.contains("command not found"))
|
if (stdOut.contains("command not found"))
|
||||||
@@ -648,6 +660,11 @@ ServerController::Vars ServerController::genVarsForScript(const ServerCredential
|
|||||||
|
|
||||||
vars.append({ { "$COOKIE_REPLY_PACKET_JUNK_SIZE", amneziaWireguarConfig.value(config_key::cookieReplyPacketJunkSize).toString() } });
|
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({ { "$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
|
// Socks5 proxy vars
|
||||||
vars.append({ { "$SOCKS5_PROXY_PORT", socks5ProxyConfig.value(config_key::port).toString(protocols::socks5Proxy::defaultPort) } });
|
vars.append({ { "$SOCKS5_PROXY_PORT", socks5ProxyConfig.value(config_key::port).toString(protocols::socks5Proxy::defaultPort) } });
|
||||||
@@ -657,7 +674,8 @@ ServerController::Vars ServerController::genVarsForScript(const ServerCredential
|
|||||||
vars.append({ { "$SOCKS5_USER", socks5user } });
|
vars.append({ { "$SOCKS5_USER", socks5user } });
|
||||||
vars.append({ { "$SOCKS5_AUTH_TYPE", socks5user.isEmpty() ? "none" : "strong" } });
|
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)
|
? NetworkUtilities::getIPAddress(credentials.hostName)
|
||||||
: credentials.hostName;
|
: credentials.hostName;
|
||||||
if (!serverIp.isEmpty()) {
|
if (!serverIp.isEmpty()) {
|
||||||
|
|||||||
@@ -99,11 +99,12 @@ QJsonObject VpnConfigurationsController::createVpnConfiguration(const QPair<QStr
|
|||||||
protocolConfigString = configurator->processConfigWithLocalSettings(dns, isApiConfig, protocolConfigString);
|
protocolConfigString = configurator->processConfigWithLocalSettings(dns, isApiConfig, protocolConfigString);
|
||||||
|
|
||||||
QJsonObject vpnConfigData = QJsonDocument::fromJson(protocolConfigString.toUtf8()).object();
|
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
|
// add mtu for old configs
|
||||||
if (vpnConfigData[config_key::mtu].toString().isEmpty()) {
|
if (vpnConfigData[config_key::mtu].toString().isEmpty()) {
|
||||||
vpnConfigData[config_key::mtu] =
|
vpnConfigData[config_key::mtu] =
|
||||||
container == DockerContainer::Awg ? protocols::awg::defaultMtu : protocols::wireguard::defaultMtu;
|
ContainerProps::isAwgContainer(container) ? protocols::awg::defaultMtu :
|
||||||
|
protocols::wireguard::defaultMtu;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ namespace amnezia
|
|||||||
ServerDockerOnCgroupsV2 = 211,
|
ServerDockerOnCgroupsV2 = 211,
|
||||||
ServerCgroupMountpoint = 212,
|
ServerCgroupMountpoint = 212,
|
||||||
DockerPullRateLimit = 213,
|
DockerPullRateLimit = 213,
|
||||||
|
ServerLinuxKernelTooOld = 214,
|
||||||
|
|
||||||
// Ssh connection errors
|
// Ssh connection errors
|
||||||
SshRequestDeniedError = 300,
|
SshRequestDeniedError = 300,
|
||||||
@@ -121,6 +122,10 @@ namespace amnezia
|
|||||||
ApiMigrationError = 1110,
|
ApiMigrationError = 1110,
|
||||||
ApiUpdateRequestError = 1111,
|
ApiUpdateRequestError = 1111,
|
||||||
ApiSubscriptionExpiredError = 1112,
|
ApiSubscriptionExpiredError = 1112,
|
||||||
|
ApiPurchaseError = 1113,
|
||||||
|
ApiSubscriptionNotActiveError = 1114,
|
||||||
|
ApiNoPurchasedSubscriptionsError = 1115,
|
||||||
|
ApiTrialAlreadyUsedError = 1116,
|
||||||
|
|
||||||
// QFile errors
|
// QFile errors
|
||||||
OpenError = 1200,
|
OpenError = 1200,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ QString errorString(ErrorCode code) {
|
|||||||
case(ErrorCode::ServerDockerOnCgroupsV2): errorMessage = QObject::tr("Docker error: runc doesn't work on cgroups v2"); break;
|
case(ErrorCode::ServerDockerOnCgroupsV2): errorMessage = QObject::tr("Docker error: runc doesn't work on cgroups v2"); break;
|
||||||
case(ErrorCode::ServerCgroupMountpoint): errorMessage = QObject::tr("Server error: cgroup mountpoint does not exist"); break;
|
case(ErrorCode::ServerCgroupMountpoint): errorMessage = QObject::tr("Server error: cgroup mountpoint does not exist"); break;
|
||||||
case(ErrorCode::DockerPullRateLimit): errorMessage = QObject::tr("Docker error: The pull rate limit has been reached"); break;
|
case(ErrorCode::DockerPullRateLimit): errorMessage = QObject::tr("Docker error: The pull rate limit has been reached"); break;
|
||||||
|
case(ErrorCode::ServerLinuxKernelTooOld): errorMessage = QObject::tr("Server error: Linux kernel is too old"); break;
|
||||||
|
|
||||||
// Libssh errors
|
// Libssh errors
|
||||||
case(ErrorCode::SshRequestDeniedError): errorMessage = QObject::tr("SSH request was denied"); break;
|
case(ErrorCode::SshRequestDeniedError): errorMessage = QObject::tr("SSH request was denied"); break;
|
||||||
@@ -78,6 +79,10 @@ QString errorString(ErrorCode code) {
|
|||||||
case (ErrorCode::ApiMigrationError): errorMessage = QObject::tr("A migration error has occurred. Please contact our technical support"); 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::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::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;
|
||||||
|
case (ErrorCode::ApiSubscriptionNotActiveError): errorMessage = QObject::tr("No active subscription found"); break;
|
||||||
|
case (ErrorCode::ApiNoPurchasedSubscriptionsError): errorMessage = QObject::tr("No purchased subscriptions found. Please purchase a subscription first"); break;
|
||||||
|
case (ErrorCode::ApiTrialAlreadyUsedError): errorMessage = QObject::tr("This email address has already been used to activate a trial"); break;
|
||||||
|
|
||||||
// QFile errors
|
// QFile errors
|
||||||
case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
|
case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
|
||||||
|
|||||||
@@ -1,132 +1,74 @@
|
|||||||
#include "ipcclient.h"
|
#include "ipcclient.h"
|
||||||
|
#include "ipc.h"
|
||||||
#include <QRemoteObjectNode>
|
#include <QRemoteObjectNode>
|
||||||
|
#include <QtNetwork/qlocalsocket.h>
|
||||||
IpcClient *IpcClient::m_instance = nullptr;
|
|
||||||
|
|
||||||
IpcClient::IpcClient(QObject *parent) : QObject(parent)
|
IpcClient::IpcClient(QObject *parent) : QObject(parent)
|
||||||
{
|
{
|
||||||
|
m_node.connectToNode(QUrl("local:" + amnezia::getIpcServiceUrl()));
|
||||||
|
m_interface.reset(m_node.acquire<IpcInterfaceReplica>());
|
||||||
}
|
}
|
||||||
|
|
||||||
IpcClient::~IpcClient()
|
IpcClient& IpcClient::Instance()
|
||||||
{
|
{
|
||||||
if (m_localSocket)
|
thread_local IpcClient ipcClient;
|
||||||
m_localSocket->close();
|
return ipcClient;
|
||||||
}
|
|
||||||
|
|
||||||
bool IpcClient::isSocketConnected() const
|
|
||||||
{
|
|
||||||
return m_isSocketConnected;
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcClient *IpcClient::Instance()
|
|
||||||
{
|
|
||||||
return m_instance;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QSharedPointer<IpcInterfaceReplica> IpcClient::Interface()
|
QSharedPointer<IpcInterfaceReplica> IpcClient::Interface()
|
||||||
{
|
{
|
||||||
if (!Instance())
|
QSharedPointer<IpcInterfaceReplica> rep = Instance().m_interface;
|
||||||
return nullptr;
|
if (rep.isNull()) {
|
||||||
return Instance()->m_ipcClient;
|
qCritical() << "IpcClient::Interface(): Failed to acquire replica";
|
||||||
}
|
|
||||||
|
|
||||||
QSharedPointer<IpcProcessTun2SocksReplica> IpcClient::InterfaceTun2Socks()
|
|
||||||
{
|
|
||||||
if (!Instance())
|
|
||||||
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";
|
|
||||||
|
|
||||||
return (Instance()->m_ipcClient->isReplicaValid() && Instance()->m_Tun2SocksClient->isReplicaValid());
|
|
||||||
}
|
|
||||||
|
|
||||||
QSharedPointer<PrivilegedProcess> IpcClient::CreatePrivilegedProcess()
|
|
||||||
{
|
|
||||||
if (!Instance()->m_ipcClient || !Instance()->m_ipcClient->isReplicaValid()) {
|
|
||||||
qWarning() << "IpcClient::createPrivilegedProcess : IpcClient IpcClient replica is not valid";
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
if (!rep->waitForSource(1000)) {
|
||||||
|
qCritical() << "IpcClient::Interface(): Failed to initialize replica";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (!rep->isReplicaValid()) {
|
||||||
|
qWarning() << "IpcClient::Interface(): Replica is invalid";
|
||||||
|
}
|
||||||
|
return rep;
|
||||||
|
}
|
||||||
|
|
||||||
QRemoteObjectPendingReply<int> futureResult = Instance()->m_ipcClient->createPrivilegedProcess();
|
QSharedPointer<IpcProcessInterfaceReplica> IpcClient::CreatePrivilegedProcess()
|
||||||
futureResult.waitForFinished(5000);
|
{
|
||||||
|
return withInterface([](QSharedPointer<IpcInterfaceReplica> &iface) -> QSharedPointer<IpcProcessInterfaceReplica> {
|
||||||
|
auto createPrivilegedProcess = iface->createPrivilegedProcess();
|
||||||
|
if (!createPrivilegedProcess.waitForFinished()) {
|
||||||
|
qCritical() << "Failed to create privileged process";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
int pid = futureResult.returnValue();
|
const int pid = createPrivilegedProcess.returnValue();
|
||||||
|
|
||||||
auto pd = QSharedPointer<ProcessDescriptor>(new ProcessDescriptor());
|
auto* node = new QRemoteObjectNode();
|
||||||
Instance()->m_processNodes.insert(pid, pd);
|
node->connectToNode(QUrl(QString("local:%1").arg(amnezia::getIpcProcessUrl(pid))));
|
||||||
|
|
||||||
pd->localSocket.reset(new QLocalSocket(pd->replicaNode.data()));
|
QSharedPointer<IpcProcessInterfaceReplica> rep(
|
||||||
|
node->acquire<IpcProcessInterfaceReplica>(),
|
||||||
connect(pd->localSocket.data(), &QLocalSocket::connected, pd->replicaNode.data(), [pd]() {
|
[node] (IpcProcessInterfaceReplica *ptr) {
|
||||||
pd->replicaNode->addClientSideConnection(pd->localSocket.data());
|
delete ptr;
|
||||||
|
node->deleteLater();
|
||||||
IpcProcessInterfaceReplica *repl = pd->replicaNode->acquire<IpcProcessInterfaceReplica>();
|
|
||||||
PrivilegedProcess *priv = static_cast<PrivilegedProcess *>(repl);
|
|
||||||
pd->ipcProcess.reset(priv);
|
|
||||||
if (!pd->ipcProcess) {
|
|
||||||
qWarning() << "Acquire PrivilegedProcess failed";
|
|
||||||
} else {
|
|
||||||
pd->ipcProcess->waitForSource(1000);
|
|
||||||
if (!pd->ipcProcess->isReplicaValid()) {
|
|
||||||
qWarning() << "PrivilegedProcess replica is not connected!";
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
QObject::connect(pd->ipcProcess.data(), &PrivilegedProcess::destroyed, pd->ipcProcess.data(),
|
if (rep.isNull()) {
|
||||||
[pd]() { pd->replicaNode->deleteLater(); });
|
qCritical() << "IpcClient::CreatePrivilegedProcess(): Failed to acquire replica";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (!rep->waitForSource()) {
|
||||||
|
qCritical() << "IpcClient::CreatePrivilegedProcess(): Failed to initialize replica";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (!rep->isReplicaValid()) {
|
||||||
|
qCritical() << "IpcClient::CreatePrivilegedProcess(): Replica is invalid";
|
||||||
|
return nullptr;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
pd->localSocket->connectToServer(amnezia::getIpcProcessUrl(pid));
|
|
||||||
pd->localSocket->waitForConnected();
|
|
||||||
|
|
||||||
auto processReplica = QSharedPointer<PrivilegedProcess>(pd->ipcProcess);
|
return rep;
|
||||||
return processReplica;
|
},
|
||||||
|
[]() -> QSharedPointer<IpcProcessInterfaceReplica> {
|
||||||
|
return nullptr;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,53 +4,53 @@
|
|||||||
#include <QLocalSocket>
|
#include <QLocalSocket>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
|
||||||
#include "ipc.h"
|
|
||||||
#include "rep_ipc_interface_replica.h"
|
#include "rep_ipc_interface_replica.h"
|
||||||
#include "rep_ipc_process_tun2socks_replica.h"
|
#include "rep_ipc_process_interface_replica.h"
|
||||||
|
|
||||||
#include "privileged_process.h"
|
|
||||||
|
|
||||||
class IpcClient : public QObject
|
class IpcClient : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit IpcClient(QObject *parent = nullptr);
|
explicit IpcClient(QObject *parent = nullptr);
|
||||||
|
|
||||||
static IpcClient *Instance();
|
static IpcClient& Instance();
|
||||||
static bool init(IpcClient *instance);
|
|
||||||
static QSharedPointer<IpcInterfaceReplica> Interface();
|
|
||||||
static QSharedPointer<IpcProcessTun2SocksReplica> InterfaceTun2Socks();
|
|
||||||
static QSharedPointer<PrivilegedProcess> CreatePrivilegedProcess();
|
|
||||||
|
|
||||||
bool isSocketConnected() const;
|
static QSharedPointer<IpcInterfaceReplica> Interface();
|
||||||
|
static QSharedPointer<IpcProcessInterfaceReplica> 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:
|
signals:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
~IpcClient() override;
|
QRemoteObjectNode m_node;
|
||||||
|
QSharedPointer<IpcInterfaceReplica> m_interface;
|
||||||
QRemoteObjectNode m_ClientNode;
|
|
||||||
QRemoteObjectNode m_Tun2SocksNode;
|
|
||||||
QSharedPointer<IpcInterfaceReplica> m_ipcClient;
|
|
||||||
QPointer<QLocalSocket> m_localSocket;
|
|
||||||
QPointer<QLocalSocket> m_tun2socksSocket;
|
|
||||||
QSharedPointer<IpcProcessTun2SocksReplica> m_Tun2SocksClient;
|
|
||||||
|
|
||||||
struct ProcessDescriptor {
|
|
||||||
ProcessDescriptor () {
|
|
||||||
replicaNode = QSharedPointer<QRemoteObjectNode>(new QRemoteObjectNode());
|
|
||||||
ipcProcess = QSharedPointer<PrivilegedProcess>();
|
|
||||||
localSocket = QSharedPointer<QLocalSocket>();
|
|
||||||
}
|
|
||||||
QSharedPointer<PrivilegedProcess> ipcProcess;
|
|
||||||
QSharedPointer<QRemoteObjectNode> replicaNode;
|
|
||||||
QSharedPointer<QLocalSocket> localSocket;
|
|
||||||
};
|
|
||||||
|
|
||||||
QMap<int, QSharedPointer<ProcessDescriptor>> m_processNodes;
|
|
||||||
bool m_isSocketConnected {false};
|
|
||||||
|
|
||||||
static IpcClient *m_instance;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // IPCCLIENT_H
|
#endif // IPCCLIENT_H
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
#include "networkUtilities.h"
|
#include "networkUtilities.h"
|
||||||
|
#include <QtNetwork/qnetworkinterface.h>
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
#ifdef Q_OS_WIN
|
#ifdef Q_OS_WIN
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#include <Ipexport.h>
|
#include <Ipexport.h>
|
||||||
#include <Ws2tcpip.h>
|
#include <Ws2tcpip.h>
|
||||||
#include <ws2ipdef.h>
|
#include <ws2ipdef.h>
|
||||||
#include <stdint.h>
|
|
||||||
#include <Iphlpapi.h>
|
#include <Iphlpapi.h>
|
||||||
#include <Iptypes.h>
|
#include <Iptypes.h>
|
||||||
#include <WinSock2.h>
|
#include <WinSock2.h>
|
||||||
@@ -30,6 +31,15 @@
|
|||||||
#include <netinet/in.h>
|
#include <netinet/in.h>
|
||||||
#include <arpa/inet.h>
|
#include <arpa/inet.h>
|
||||||
#include <net/route.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
|
#endif
|
||||||
|
|
||||||
#include <QHostAddress>
|
#include <QHostAddress>
|
||||||
@@ -170,7 +180,7 @@ int NetworkUtilities::AdapterIndexTo(const QHostAddress& dst) {
|
|||||||
#ifdef Q_OS_WIN
|
#ifdef Q_OS_WIN
|
||||||
qDebug() << "Getting Current Internet Adapter that routes to"
|
qDebug() << "Getting Current Internet Adapter that routes to"
|
||||||
<< dst.toString();
|
<< dst.toString();
|
||||||
quint32_be ipBigEndian;
|
quint32 ipBigEndian;
|
||||||
quint32 ip = dst.toIPv4Address();
|
quint32 ip = dst.toIPv4Address();
|
||||||
qToBigEndian(ip, &ipBigEndian);
|
qToBigEndian(ip, &ipBigEndian);
|
||||||
_MIB_IPFORWARDROW routeInfo;
|
_MIB_IPFORWARDROW routeInfo;
|
||||||
@@ -239,12 +249,14 @@ DWORD GetAdaptersAddressesWrapper(const ULONG Family,
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
QString NetworkUtilities::getGatewayAndIface()
|
QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
|
||||||
{
|
{
|
||||||
#ifdef Q_OS_WIN
|
#ifdef Q_OS_WIN
|
||||||
constexpr int BUFF_LEN = 100;
|
constexpr int BUFF_LEN = 100;
|
||||||
char buff[BUFF_LEN] = {'\0'};
|
char buff[BUFF_LEN] = {'\0'};
|
||||||
QString result;
|
|
||||||
|
QString resGateway;
|
||||||
|
int resIndex = -1;
|
||||||
|
|
||||||
PIP_ADAPTER_ADDRESSES pAdapterAddresses = nullptr;
|
PIP_ADAPTER_ADDRESSES pAdapterAddresses = nullptr;
|
||||||
DWORD dwRetVal =
|
DWORD dwRetVal =
|
||||||
@@ -252,7 +264,7 @@ QString NetworkUtilities::getGatewayAndIface()
|
|||||||
|
|
||||||
if (dwRetVal != NO_ERROR) {
|
if (dwRetVal != NO_ERROR) {
|
||||||
qDebug() << "ipv4 stack detect GetAdaptersAddresses failed.";
|
qDebug() << "ipv4 stack detect GetAdaptersAddresses failed.";
|
||||||
return "";
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
PIP_ADAPTER_ADDRESSES pCurAddress = pAdapterAddresses;
|
PIP_ADAPTER_ADDRESSES pCurAddress = pAdapterAddresses;
|
||||||
@@ -267,7 +279,9 @@ QString NetworkUtilities::getGatewayAndIface()
|
|||||||
struct sockaddr_in addr;
|
struct sockaddr_in addr;
|
||||||
if (inet_pton(AF_INET, buff, &addr.sin_addr) == 1) {
|
if (inet_pton(AF_INET, buff, &addr.sin_addr) == 1) {
|
||||||
qDebug() << "this is true v4 !";
|
qDebug() << "this is true v4 !";
|
||||||
result = gw;
|
|
||||||
|
resGateway = gw;
|
||||||
|
resIndex = pCurAddress->IfIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -275,7 +289,7 @@ QString NetworkUtilities::getGatewayAndIface()
|
|||||||
}
|
}
|
||||||
|
|
||||||
free(pAdapterAddresses);
|
free(pAdapterAddresses);
|
||||||
return result;
|
return { resGateway, QNetworkInterface::interfaceFromIndex(resIndex) };
|
||||||
#endif
|
#endif
|
||||||
#ifdef Q_OS_LINUX
|
#ifdef Q_OS_LINUX
|
||||||
constexpr int BUFFER_SIZE = 100;
|
constexpr int BUFFER_SIZE = 100;
|
||||||
@@ -292,7 +306,7 @@ QString NetworkUtilities::getGatewayAndIface()
|
|||||||
|
|
||||||
if ((sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)) < 0) {
|
if ((sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)) < 0) {
|
||||||
perror("socket failed");
|
perror("socket failed");
|
||||||
return "";
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
memset(msgbuf, 0, sizeof(msgbuf));
|
memset(msgbuf, 0, sizeof(msgbuf));
|
||||||
@@ -316,7 +330,7 @@ QString NetworkUtilities::getGatewayAndIface()
|
|||||||
/* send msg */
|
/* send msg */
|
||||||
if (send(sock, nlmsg, nlmsg->nlmsg_len, 0) < 0) {
|
if (send(sock, nlmsg, nlmsg->nlmsg_len, 0) < 0) {
|
||||||
perror("send failed");
|
perror("send failed");
|
||||||
return "";
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* receive response */
|
/* receive response */
|
||||||
@@ -325,7 +339,7 @@ QString NetworkUtilities::getGatewayAndIface()
|
|||||||
received_bytes = recv(sock, ptr, sizeof(buffer) - msg_len, 0);
|
received_bytes = recv(sock, ptr, sizeof(buffer) - msg_len, 0);
|
||||||
if (received_bytes < 0) {
|
if (received_bytes < 0) {
|
||||||
perror("Error in recv");
|
perror("Error in recv");
|
||||||
return "";
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
nlh = (struct nlmsghdr *) ptr;
|
nlh = (struct nlmsghdr *) ptr;
|
||||||
@@ -335,7 +349,7 @@ QString NetworkUtilities::getGatewayAndIface()
|
|||||||
(nlmsg->nlmsg_type == NLMSG_ERROR))
|
(nlmsg->nlmsg_type == NLMSG_ERROR))
|
||||||
{
|
{
|
||||||
perror("Error in received packet");
|
perror("Error in received packet");
|
||||||
return "";
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* If we received all data break */
|
/* If we received all data break */
|
||||||
@@ -388,10 +402,12 @@ QString NetworkUtilities::getGatewayAndIface()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
close(sock);
|
close(sock);
|
||||||
return gateway_address;
|
return { gateway_address, QNetworkInterface::interfaceFromName(interface) };
|
||||||
#endif
|
#endif
|
||||||
#if defined(Q_OS_MAC) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
#if defined(Q_OS_MAC) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
||||||
QString gateway;
|
QString gateway;
|
||||||
|
int index = -1;
|
||||||
|
|
||||||
int mib[] = {CTL_NET, PF_ROUTE, 0, 0, NET_RT_FLAGS, RTF_GATEWAY};
|
int mib[] = {CTL_NET, PF_ROUTE, 0, 0, NET_RT_FLAGS, RTF_GATEWAY};
|
||||||
int afinet_type[] = {AF_INET, AF_INET6};
|
int afinet_type[] = {AF_INET, AF_INET6};
|
||||||
|
|
||||||
@@ -401,17 +417,17 @@ QString NetworkUtilities::getGatewayAndIface()
|
|||||||
|
|
||||||
size_t needed = 0;
|
size_t needed = 0;
|
||||||
if (sysctl(mib, sizeof(mib) / sizeof(int), nullptr, &needed, nullptr, 0) < 0)
|
if (sysctl(mib, sizeof(mib) / sizeof(int), nullptr, &needed, nullptr, 0) < 0)
|
||||||
return "";
|
return {};
|
||||||
|
|
||||||
char* buf;
|
char* buf;
|
||||||
if ((buf = new char[needed]) == 0)
|
if ((buf = new char[needed]) == 0)
|
||||||
return "";
|
return {};
|
||||||
|
|
||||||
if (sysctl(mib, sizeof(mib) / sizeof(int), buf, &needed, nullptr, 0) < 0)
|
if (sysctl(mib, sizeof(mib) / sizeof(int), buf, &needed, nullptr, 0) < 0)
|
||||||
{
|
{
|
||||||
qDebug() << "sysctl: net.route.0.0.dump";
|
qDebug() << "sysctl: net.route.0.0.dump";
|
||||||
delete[] buf;
|
delete[] buf;
|
||||||
return gateway;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
struct rt_msghdr* rt;
|
struct rt_msghdr* rt;
|
||||||
@@ -449,7 +465,10 @@ QString NetworkUtilities::getGatewayAndIface()
|
|||||||
&(reinterpret_cast<struct sockaddr_in*>(sa_tab[RTAX_GATEWAY]))->sin_addr,
|
&(reinterpret_cast<struct sockaddr_in*>(sa_tab[RTAX_GATEWAY]))->sin_addr,
|
||||||
sizeof(struct in_addr));
|
sizeof(struct in_addr));
|
||||||
if (inet_ntop(AF_INET, srcStr4, dstStr4, INET_ADDRSTRLEN) != nullptr)
|
if (inet_ntop(AF_INET, srcStr4, dstStr4, INET_ADDRSTRLEN) != nullptr)
|
||||||
|
{
|
||||||
gateway = dstStr4;
|
gateway = dstStr4;
|
||||||
|
index = rt->rtm_index;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -463,7 +482,10 @@ QString NetworkUtilities::getGatewayAndIface()
|
|||||||
&(reinterpret_cast<struct sockaddr_in6*>(sa_tab[RTAX_GATEWAY]))->sin6_addr,
|
&(reinterpret_cast<struct sockaddr_in6*>(sa_tab[RTAX_GATEWAY]))->sin6_addr,
|
||||||
sizeof(struct in6_addr));
|
sizeof(struct in6_addr));
|
||||||
if (inet_ntop(AF_INET6, srcStr6, dstStr6, INET6_ADDRSTRLEN) != nullptr)
|
if (inet_ntop(AF_INET6, srcStr6, dstStr6, INET6_ADDRSTRLEN) != nullptr)
|
||||||
|
{
|
||||||
gateway = dstStr6;
|
gateway = dstStr6;
|
||||||
|
index = rt->rtm_index;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -472,6 +494,6 @@ QString NetworkUtilities::getGatewayAndIface()
|
|||||||
free(buf);
|
free(buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
return gateway;
|
return { gateway, QNetworkInterface::interfaceFromIndex(index) };
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QHostAddress>
|
#include <QHostAddress>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
|
#include <QtNetwork/qnetworkinterface.h>
|
||||||
|
|
||||||
class NetworkUtilities : public QObject
|
class NetworkUtilities : public QObject
|
||||||
{
|
{
|
||||||
@@ -17,7 +17,7 @@ public:
|
|||||||
static bool checkIPv4Format(const QString &ip);
|
static bool checkIPv4Format(const QString &ip);
|
||||||
static bool checkIpSubnetFormat(const QString &ip);
|
static bool checkIpSubnetFormat(const QString &ip);
|
||||||
static bool checkIpv6Enabled();
|
static bool checkIpv6Enabled();
|
||||||
static QString getGatewayAndIface();
|
static QPair<QString, QNetworkInterface> getGatewayAndIface();
|
||||||
// Returns the Interface Index that could Route to dst
|
// Returns the Interface Index that could Route to dst
|
||||||
static int AdapterIndexTo(const QHostAddress& 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
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
#include "privileged_process.h"
|
|
||||||
|
|
||||||
PrivilegedProcess::PrivilegedProcess() :
|
|
||||||
IpcProcessInterfaceReplica()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
PrivilegedProcess::~PrivilegedProcess()
|
|
||||||
{
|
|
||||||
qDebug() << "PrivilegedProcess::~PrivilegedProcess()";
|
|
||||||
}
|
|
||||||
|
|
||||||
void PrivilegedProcess::waitForFinished(int msecs)
|
|
||||||
{
|
|
||||||
QSharedPointer<QEventLoop> loop(new QEventLoop);
|
|
||||||
connect(this, &PrivilegedProcess::finished, this, [this, loop](int exitCode, QProcess::ExitStatus exitStatus) mutable{
|
|
||||||
loop->quit();
|
|
||||||
loop.clear();
|
|
||||||
});
|
|
||||||
|
|
||||||
QTimer::singleShot(msecs, this, [this, loop]() mutable {
|
|
||||||
loop->quit();
|
|
||||||
loop.clear();
|
|
||||||
});
|
|
||||||
|
|
||||||
loop->exec();
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
#ifndef PRIVILEGED_PROCESS_H
|
|
||||||
#define PRIVILEGED_PROCESS_H
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
|
|
||||||
#include "rep_ipc_process_interface_replica.h"
|
|
||||||
// This class is dangerous - instance of this class casted from base class,
|
|
||||||
// so it support only functions
|
|
||||||
// Do not add any members into it
|
|
||||||
//
|
|
||||||
class PrivilegedProcess : public IpcProcessInterfaceReplica
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
PrivilegedProcess();
|
|
||||||
~PrivilegedProcess() override;
|
|
||||||
|
|
||||||
void waitForFinished(int msecs);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // PRIVILEGED_PROCESS_H
|
|
||||||
|
|
||||||
|
|
||||||
@@ -11,7 +11,8 @@ QString amnezia::scriptFolder(amnezia::DockerContainer container)
|
|||||||
case DockerContainer::Cloak: return QLatin1String("openvpn_cloak");
|
case DockerContainer::Cloak: return QLatin1String("openvpn_cloak");
|
||||||
case DockerContainer::ShadowSocks: return QLatin1String("openvpn_shadowsocks");
|
case DockerContainer::ShadowSocks: return QLatin1String("openvpn_shadowsocks");
|
||||||
case DockerContainer::WireGuard: return QLatin1String("wireguard");
|
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::Ipsec: return QLatin1String("ipsec");
|
||||||
case DockerContainer::Xray: return QLatin1String("xray");
|
case DockerContainer::Xray: return QLatin1String("xray");
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <QJsonArray>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
|
#include <QHostAddress>
|
||||||
|
#include <QRandomGenerator>
|
||||||
|
#include <QTcpServer>
|
||||||
|
#include <stdexcept>
|
||||||
#include "3rd/QJsonStruct/QJsonIO.hpp"
|
#include "3rd/QJsonStruct/QJsonIO.hpp"
|
||||||
#include "transfer.h"
|
#include "transfer.h"
|
||||||
#include "serialization.h"
|
#include "serialization.h"
|
||||||
@@ -14,25 +19,125 @@ namespace amnezia::serialization::inbounds
|
|||||||
// "port": 10808,
|
// "port": 10808,
|
||||||
// "protocol": "socks",
|
// "protocol": "socks",
|
||||||
// "settings": {
|
// "settings": {
|
||||||
|
// "auth": "password",
|
||||||
|
// "accounts": [{"user": "...", "pass": "..."}],
|
||||||
// "udp": true
|
// "udp": true
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
//],
|
//],
|
||||||
|
|
||||||
const static QString listen = "127.0.0.1";
|
const static QString listen = "127.0.0.1";
|
||||||
const static int port = 10808;
|
const static int defaultPort = 10808;
|
||||||
const static QString protocol = "socks";
|
const static QString protocol = "socks";
|
||||||
|
|
||||||
|
static int indexOfSocksInbound(const QJsonArray &inbounds)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < inbounds.size(); ++i) {
|
||||||
|
const QString p = inbounds.at(i).toObject().value(QLatin1String("protocol")).toString();
|
||||||
|
if (p.compare(QLatin1String("socks"), Qt::CaseInsensitive) == 0)
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask the OS for a free TCP port on loopback (same stack as inbound "listen": "127.0.0.1").
|
||||||
|
static int acquireFreeLocalPort()
|
||||||
|
{
|
||||||
|
QTcpServer probe;
|
||||||
|
if (!probe.listen(QHostAddress(QStringLiteral("127.0.0.1")), 0)) {
|
||||||
|
throw std::runtime_error(
|
||||||
|
"Failed to bind a local TCP port on 127.0.0.1 for SOCKS inbound "
|
||||||
|
"(QTcpServer::listen failed; possible permission or OS network error).");
|
||||||
|
}
|
||||||
|
return static_cast<int>(probe.serverPort());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates a hex string of `byteCount` random bytes (URL-safe, no special chars).
|
||||||
|
static QString generateRandomHex(int byteCount)
|
||||||
|
{
|
||||||
|
if (byteCount <= 0)
|
||||||
|
return {};
|
||||||
|
// fillRange writes full quint32 words; size the buffer to a multiple of 4 bytes to avoid
|
||||||
|
// overrunning a short buffer when byteCount is not divisible by 4.
|
||||||
|
const int numUint32 = (byteCount + int(sizeof(quint32)) - 1) / int(sizeof(quint32));
|
||||||
|
QByteArray buf(numUint32 * int(sizeof(quint32)), '\0');
|
||||||
|
QRandomGenerator::system()->fillRange(reinterpret_cast<quint32 *>(buf.data()), numUint32);
|
||||||
|
return QString::fromLatin1(buf.left(byteCount).toHex());
|
||||||
|
}
|
||||||
|
|
||||||
QJsonObject GenerateInboundEntry()
|
QJsonObject GenerateInboundEntry()
|
||||||
{
|
{
|
||||||
QJsonObject root;
|
QJsonObject root;
|
||||||
QJsonIO::SetValue(root, listen, "listen");
|
QJsonIO::SetValue(root, listen, "listen");
|
||||||
QJsonIO::SetValue(root, port, "port");
|
QJsonIO::SetValue(root, defaultPort, "port");
|
||||||
QJsonIO::SetValue(root, protocol, "protocol");
|
QJsonIO::SetValue(root, protocol, "protocol");
|
||||||
QJsonIO::SetValue(root, true, "settings", "udp");
|
QJsonIO::SetValue(root, true, "settings", "udp");
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
InboundCredentials GetInboundCredentials(const QJsonObject &xrayConfig)
|
||||||
|
{
|
||||||
|
InboundCredentials creds;
|
||||||
|
creds.port = defaultPort;
|
||||||
|
|
||||||
|
const QJsonArray inbounds = xrayConfig.value("inbounds").toArray();
|
||||||
|
const int socksIdx = indexOfSocksInbound(inbounds);
|
||||||
|
if (socksIdx < 0)
|
||||||
|
return creds;
|
||||||
|
|
||||||
|
const QJsonObject inbound = inbounds.at(socksIdx).toObject();
|
||||||
|
creds.port = inbound.value("port").toInt(defaultPort);
|
||||||
|
|
||||||
|
const QJsonObject settings = inbound.value("settings").toObject();
|
||||||
|
const QJsonArray accounts = settings.value("accounts").toArray();
|
||||||
|
if (accounts.isEmpty())
|
||||||
|
return creds;
|
||||||
|
|
||||||
|
const QJsonObject account = accounts.first().toObject();
|
||||||
|
creds.username = account.value("user").toString();
|
||||||
|
creds.password = account.value("pass").toString();
|
||||||
|
return creds;
|
||||||
|
}
|
||||||
|
|
||||||
|
InboundCredentials EnsureInboundAuth(QJsonObject &xrayConfig)
|
||||||
|
{
|
||||||
|
QJsonArray inbounds = xrayConfig.value("inbounds").toArray();
|
||||||
|
const int socksIdx = indexOfSocksInbound(inbounds);
|
||||||
|
if (socksIdx < 0)
|
||||||
|
return GetInboundCredentials(xrayConfig); // no SOCKS inbound to patch
|
||||||
|
|
||||||
|
QJsonObject inbound = inbounds.at(socksIdx).toObject();
|
||||||
|
InboundCredentials creds;
|
||||||
|
creds.port = acquireFreeLocalPort();
|
||||||
|
inbound["port"] = creds.port;
|
||||||
|
|
||||||
|
QJsonObject settings = inbound.value("settings").toObject();
|
||||||
|
const QJsonArray accounts = settings.value("accounts").toArray();
|
||||||
|
if (!accounts.isEmpty()) {
|
||||||
|
const QJsonObject account = accounts.first().toObject();
|
||||||
|
creds.username = account.value("user").toString();
|
||||||
|
creds.password = account.value("pass").toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (creds.username.isEmpty() || creds.password.isEmpty()) {
|
||||||
|
// Generate fresh credentials for this session (never persisted)
|
||||||
|
creds.username = generateRandomHex(8); // 16 hex chars
|
||||||
|
creds.password = generateRandomHex(16); // 32 hex chars
|
||||||
|
QJsonObject account;
|
||||||
|
account["user"] = creds.username;
|
||||||
|
account["pass"] = creds.password;
|
||||||
|
settings["accounts"] = QJsonArray{ account };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always ensure auth mode is enforced, even for imported configs that had
|
||||||
|
// accounts but auth: "noauth" (or no auth field at all).
|
||||||
|
settings["auth"] = QStringLiteral("password");
|
||||||
|
inbound["settings"] = settings;
|
||||||
|
inbounds[socksIdx] = inbound;
|
||||||
|
xrayConfig["inbounds"] = inbounds;
|
||||||
|
|
||||||
|
return creds;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace amnezia::serialization::inbounds
|
} // namespace amnezia::serialization::inbounds
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ namespace amnezia::serialization
|
|||||||
namespace vless
|
namespace vless
|
||||||
{
|
{
|
||||||
QJsonObject Deserialize(const QString &vless, QString *alias, QString *errMessage);
|
QJsonObject Deserialize(const QString &vless, QString *alias, QString *errMessage);
|
||||||
|
const QString Serialize(const VlessServerObject &server, const QString &alias);
|
||||||
} // namespace vless
|
} // namespace vless
|
||||||
|
|
||||||
namespace ss
|
namespace ss
|
||||||
@@ -59,7 +60,24 @@ namespace amnezia::serialization
|
|||||||
|
|
||||||
namespace inbounds
|
namespace inbounds
|
||||||
{
|
{
|
||||||
|
struct InboundCredentials {
|
||||||
|
QString username;
|
||||||
|
QString password;
|
||||||
|
int port;
|
||||||
|
};
|
||||||
|
|
||||||
QJsonObject GenerateInboundEntry();
|
QJsonObject GenerateInboundEntry();
|
||||||
|
|
||||||
|
// Reads existing SOCKS5 auth from the first inbound with protocol "socks"
|
||||||
|
// (.settings.accounts[0]). Returns empty username/password if none.
|
||||||
|
InboundCredentials GetInboundCredentials(const QJsonObject &xrayConfig);
|
||||||
|
|
||||||
|
// Ensures SOCKS5 auth is present on the inbound whose protocol is "socks".
|
||||||
|
// Re-uses existing credentials if already set; otherwise generates random ones
|
||||||
|
// and writes them into the config. Assigns a free loopback TCP port each session
|
||||||
|
// (OS-assigned). Throws std::runtime_error if a SOCKS inbound exists but binding
|
||||||
|
// a local port on 127.0.0.1 fails (e.g. permissions or OS error).
|
||||||
|
InboundCredentials EnsureInboundAuth(QJsonObject &xrayConfig);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
namespace transfer
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|||||||
@@ -252,5 +252,65 @@ QJsonObject Deserialize(const QString &str, QString *alias, QString *errMessage)
|
|||||||
root["inbounds"] = QJsonArray { inbound };
|
root["inbounds"] = QJsonArray { inbound };
|
||||||
return root;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -440,18 +440,6 @@ bool Daemon::parseConfig(const QJsonObject& obj, InterfaceConfig& config) {
|
|||||||
if (!obj.value("I5").isNull()) {
|
if (!obj.value("I5").isNull()) {
|
||||||
config.m_specialJunk["I5"] = obj.value("I5").toString();
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,12 +152,6 @@ QString InterfaceConfig::toWgConf(const QMap<QString, QString>& extra) const {
|
|||||||
for (const QString& key : m_specialJunk.keys()) {
|
for (const QString& key : m_specialJunk.keys()) {
|
||||||
out << key << " = " << m_specialJunk[key] << "\n";
|
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.
|
// If any extra config was provided, append it now.
|
||||||
for (const QString& key : extra.keys()) {
|
for (const QString& key : extra.keys()) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <QMap>
|
||||||
#include "ipaddress.h"
|
#include "ipaddress.h"
|
||||||
|
|
||||||
class QJsonObject;
|
class QJsonObject;
|
||||||
@@ -57,8 +57,6 @@ class InterfaceConfig {
|
|||||||
QString m_underloadPacketMagicHeader;
|
QString m_underloadPacketMagicHeader;
|
||||||
QString m_transportPacketMagicHeader;
|
QString m_transportPacketMagicHeader;
|
||||||
QMap<QString, QString> m_specialJunk;
|
QMap<QString, QString> m_specialJunk;
|
||||||
QMap<QString, QString> m_controlledJunk;
|
|
||||||
QString m_specialHandshakeTimeout;
|
|
||||||
|
|
||||||
QJsonObject toJson() const;
|
QJsonObject toJson() const;
|
||||||
QString toWgConf(
|
QString toWgConf(
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M15 21V17C15 16.4696 15.2107 15.9609 15.5858 15.5858C15.9609 15.2107 16.4696 15 17 15H21" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M7 4V6C7.21572 6.61347 7.62494 7.14024 8.16602 7.50096C8.7071 7.86168 9.35075 8.03682 10 8V8C10.5304 8 11.0391 8.21071 11.4142 8.58579C11.7893 8.96086 12 9.46957 12 10C12 10.5304 12.2107 11.0391 12.5858 11.4142C12.9609 11.7893 13.4696 12 14 12C14.5304 12 15.0391 11.7893 15.4142 11.4142C15.7893 11.0391 16 10.5304 16 10C16 9.46957 16.2107 8.96086 16.5858 8.58579C16.9609 8.21071 17.4696 8 18 8H21" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M3 11H5C5.53043 11 6.03914 11.2107 6.41421 11.5858C6.78929 11.9609 7 12.4696 7 13V14C7 14.5304 7.21071 15.0391 7.58579 15.4142C7.96086 15.7893 8.46957 16 9 16C9.53043 16 10.0391 16.2107 10.4142 16.5858C10.7893 16.9609 11 17.4696 11 18V22" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M18.1777 8C23.2737 8 23.2737 16 18.1777 16C13.0827 16 11.0447 8 5.43875 8C0.85375 8 0.85375 16 5.43875 16C11.0447 16 13.0828 8 18.1788 8H18.1777Z" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 342 B |
@@ -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 |
|
After Width: | Height: | Size: 5.9 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M17 2H7C5.89543 2 5 2.89543 5 4V20C5 21.1046 5.89543 22 7 22H17C18.1046 22 19 21.1046 19 20V4C19 2.89543 18.1046 2 17 2Z" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M12 18H12.01" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 423 B |
@@ -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 |
@@ -16,7 +16,7 @@ set_target_properties(AmneziaVPNNetworkExtension PROPERTIES
|
|||||||
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_NAME "${BUILD_IOS_APP_IDENTIFIER}.network-extension"
|
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_NAME "${BUILD_IOS_APP_IDENTIFIER}.network-extension"
|
||||||
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS ${CMAKE_CURRENT_SOURCE_DIR}/AmneziaVPNNetworkExtension.entitlements
|
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS ${CMAKE_CURRENT_SOURCE_DIR}/AmneziaVPNNetworkExtension.entitlements
|
||||||
XCODE_ATTRIBUTE_MARKETING_VERSION "${APP_MAJOR_VERSION}"
|
XCODE_ATTRIBUTE_MARKETING_VERSION "${APP_MAJOR_VERSION}"
|
||||||
XCODE_ATTRIBUTE_CURRENT_PROJECT_VERSION "${BUILD_ID}"
|
XCODE_ATTRIBUTE_CURRENT_PROJECT_VERSION "${CMAKE_PROJECT_VERSION_TWEAK}"
|
||||||
XCODE_ATTRIBUTE_PRODUCT_NAME "AmneziaVPNNetworkExtension"
|
XCODE_ATTRIBUTE_PRODUCT_NAME "AmneziaVPNNetworkExtension"
|
||||||
|
|
||||||
XCODE_ATTRIBUTE_APPLICATION_EXTENSION_API_ONLY "YES"
|
XCODE_ATTRIBUTE_APPLICATION_EXTENSION_API_ONLY "YES"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<string>AmneziaVPNNetworkExtension</string>
|
<string>AmneziaVPNNetworkExtension</string>
|
||||||
|
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>org.amnezia.AmneziaVPN.network-extension</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
@@ -16,9 +16,9 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>${APPLE_PROJECT_VERSION}</string>
|
<string>$(MARKETING_VERSION)</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>${CMAKE_PROJECT_VERSION_TWEAK}</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
|
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false/>
|
||||||
@@ -41,6 +41,6 @@
|
|||||||
<string>group.org.amnezia.AmneziaVPN</string>
|
<string>group.org.amnezia.AmneziaVPN</string>
|
||||||
|
|
||||||
<key>com.wireguard.macos.app_group_id</key>
|
<key>com.wireguard.macos.app_group_id</key>
|
||||||
<string>${BUILD_VPN_DEVELOPMENT_TEAM}.group.org.amnezia.AmneziaVPN</string>
|
<string>$(DEVELOPMENT_TEAM).group.org.amnezia.AmneziaVPN</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
|
||||||
#include "amnezia_application.h"
|
#include "amnezia_application.h"
|
||||||
|
#include "core/osSignalHandler.h"
|
||||||
#include "migrations.h"
|
#include "migrations.h"
|
||||||
#include "version.h"
|
#include "version.h"
|
||||||
|
|
||||||
@@ -44,6 +45,7 @@ int main(int argc, char *argv[])
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
AmneziaApplication app(argc, argv);
|
AmneziaApplication app(argc, argv);
|
||||||
|
OsSignalHandler::setup();
|
||||||
|
|
||||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
||||||
if (isAnotherInstanceRunning()) {
|
if (isAnotherInstanceRunning()) {
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QDebug>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QHostAddress>
|
#include <QHostAddress>
|
||||||
@@ -12,12 +15,13 @@
|
|||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QJsonValue>
|
#include <QJsonValue>
|
||||||
|
#include <QLocalSocket>
|
||||||
|
#include <QObject>
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
#include "ipaddress.h"
|
|
||||||
#include "leakdetector.h"
|
#include "leakdetector.h"
|
||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
#include "models/server.h"
|
|
||||||
#include "daemon/daemonerrors.h"
|
#include "daemon/daemonerrors.h"
|
||||||
|
|
||||||
#include "protocols/protocols_defs.h"
|
#include "protocols/protocols_defs.h"
|
||||||
@@ -115,7 +119,6 @@ void LocalSocketController::daemonConnected() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void LocalSocketController::activate(const QJsonObject &rawConfig) {
|
void LocalSocketController::activate(const QJsonObject &rawConfig) {
|
||||||
|
|
||||||
QString protocolName = rawConfig.value("protocol").toString();
|
QString protocolName = rawConfig.value("protocol").toString();
|
||||||
|
|
||||||
int splitTunnelType = rawConfig.value("splitTunnelType").toInt();
|
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("hopindex", QJsonValue((double)hop.m_hopindex));
|
||||||
json.insert("privateKey", wgConfig.value(amnezia::config_key::client_priv_key));
|
json.insert("privateKey", wgConfig.value(amnezia::config_key::client_priv_key));
|
||||||
json.insert("deviceIpv4Address", wgConfig.value(amnezia::config_key::client_ip));
|
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.
|
// 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.
|
// 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.
|
// Otherwise some OSes (Linux) try IPv6 forever and hang.
|
||||||
// https://en.wikipedia.org/wiki/Unique_local_address (RFC 4193)
|
// https://en.wikipedia.org/wiki/Unique_local_address (RFC 4193)
|
||||||
// https://man7.org/linux/man-pages/man5/gai.conf.5.html
|
// 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("serverPublicKey", wgConfig.value(amnezia::config_key::server_pub_key));
|
||||||
json.insert("serverPskKey", wgConfig.value(amnezia::config_key::psk_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);
|
json.insert("allowedIPAddressRanges", jsAllowedIPAddesses);
|
||||||
|
|
||||||
|
|
||||||
QJsonArray jsExcludedAddresses;
|
QJsonArray jsExcludedAddresses;
|
||||||
jsExcludedAddresses.append(wgConfig.value(amnezia::config_key::hostName));
|
jsExcludedAddresses.append(wgConfig.value(amnezia::config_key::hostName));
|
||||||
if (splitTunnelType == 2) {
|
if (splitTunnelType == 2) {
|
||||||
@@ -255,50 +260,33 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
|
|||||||
json.insert(amnezia::config_key::specialJunk3, wgConfig.value(amnezia::config_key::specialJunk3));
|
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::specialJunk4, wgConfig.value(amnezia::config_key::specialJunk4));
|
||||||
json.insert(amnezia::config_key::specialJunk5, wgConfig.value(amnezia::config_key::specialJunk5));
|
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()
|
} else if (!wgConfig.value(amnezia::config_key::junkPacketCount).isUndefined()
|
||||||
&& !wgConfig.value(amnezia::config_key::junkPacketMinSize).isUndefined()
|
&& !wgConfig.value(amnezia::config_key::junkPacketMinSize).isUndefined()
|
||||||
&& !wgConfig.value(amnezia::config_key::junkPacketMaxSize).isUndefined()
|
&& !wgConfig.value(amnezia::config_key::junkPacketMaxSize).isUndefined()
|
||||||
&& !wgConfig.value(amnezia::config_key::initPacketJunkSize).isUndefined()
|
&& !wgConfig.value(amnezia::config_key::initPacketJunkSize).isUndefined()
|
||||||
&& !wgConfig.value(amnezia::config_key::responsePacketJunkSize).isUndefined()
|
&& !wgConfig.value(amnezia::config_key::responsePacketJunkSize).isUndefined()
|
||||||
// && !wgConfig.value(amnezia::config_key::cookieReplyPacketJunkSize).isUndefined()
|
&& !wgConfig.value(amnezia::config_key::cookieReplyPacketJunkSize).isUndefined()
|
||||||
// && !wgConfig.value(amnezia::config_key::transportPacketJunkSize).isUndefined()
|
&& !wgConfig.value(amnezia::config_key::transportPacketJunkSize).isUndefined()
|
||||||
&& !wgConfig.value(amnezia::config_key::initPacketMagicHeader).isUndefined()
|
&& !wgConfig.value(amnezia::config_key::initPacketMagicHeader).isUndefined()
|
||||||
&& !wgConfig.value(amnezia::config_key::responsePacketMagicHeader).isUndefined()
|
&& !wgConfig.value(amnezia::config_key::responsePacketMagicHeader).isUndefined()
|
||||||
&& !wgConfig.value(amnezia::config_key::underloadPacketMagicHeader).isUndefined()
|
&& !wgConfig.value(amnezia::config_key::underloadPacketMagicHeader).isUndefined()
|
||||||
&& !wgConfig.value(amnezia::config_key::transportPacketMagicHeader).isUndefined()
|
&& !wgConfig.value(amnezia::config_key::transportPacketMagicHeader).isUndefined()) {
|
||||||
/* && !wgConfig.value(amnezia::config_key::specialJunk1).isUndefined()
|
|
||||||
&& !wgConfig.value(amnezia::config_key::specialJunk2).isUndefined()
|
|
||||||
&& !wgConfig.value(amnezia::config_key::specialJunk3).isUndefined()
|
|
||||||
&& !wgConfig.value(amnezia::config_key::specialJunk4).isUndefined()
|
|
||||||
&& !wgConfig.value(amnezia::config_key::specialJunk5).isUndefined()
|
|
||||||
&& !wgConfig.value(amnezia::config_key::controlledJunk1).isUndefined()
|
|
||||||
&& !wgConfig.value(amnezia::config_key::controlledJunk2).isUndefined()
|
|
||||||
&& !wgConfig.value(amnezia::config_key::controlledJunk3).isUndefined()
|
|
||||||
&& !wgConfig.value(amnezia::config_key::specialHandshakeTimeout).isUndefined()*/) {
|
|
||||||
json.insert(amnezia::config_key::junkPacketCount, wgConfig.value(amnezia::config_key::junkPacketCount));
|
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::junkPacketMinSize, wgConfig.value(amnezia::config_key::junkPacketMinSize));
|
||||||
json.insert(amnezia::config_key::junkPacketMaxSize, wgConfig.value(amnezia::config_key::junkPacketMaxSize));
|
json.insert(amnezia::config_key::junkPacketMaxSize, wgConfig.value(amnezia::config_key::junkPacketMaxSize));
|
||||||
json.insert(amnezia::config_key::initPacketJunkSize, wgConfig.value(amnezia::config_key::initPacketJunkSize));
|
json.insert(amnezia::config_key::initPacketJunkSize, wgConfig.value(amnezia::config_key::initPacketJunkSize));
|
||||||
json.insert(amnezia::config_key::responsePacketJunkSize, wgConfig.value(amnezia::config_key::responsePacketJunkSize));
|
json.insert(amnezia::config_key::responsePacketJunkSize, wgConfig.value(amnezia::config_key::responsePacketJunkSize));
|
||||||
// json.insert(amnezia::config_key::cookieReplyPacketJunkSize, wgConfig.value(amnezia::config_key::cookieReplyPacketJunkSize));
|
json.insert(amnezia::config_key::cookieReplyPacketJunkSize, wgConfig.value(amnezia::config_key::cookieReplyPacketJunkSize));
|
||||||
// json.insert(amnezia::config_key::transportPacketJunkSize, wgConfig.value(amnezia::config_key::transportPacketJunkSize));
|
json.insert(amnezia::config_key::transportPacketJunkSize, wgConfig.value(amnezia::config_key::transportPacketJunkSize));
|
||||||
json.insert(amnezia::config_key::initPacketMagicHeader, wgConfig.value(amnezia::config_key::initPacketMagicHeader));
|
json.insert(amnezia::config_key::initPacketMagicHeader, wgConfig.value(amnezia::config_key::initPacketMagicHeader));
|
||||||
json.insert(amnezia::config_key::responsePacketMagicHeader, wgConfig.value(amnezia::config_key::responsePacketMagicHeader));
|
json.insert(amnezia::config_key::responsePacketMagicHeader, wgConfig.value(amnezia::config_key::responsePacketMagicHeader));
|
||||||
json.insert(amnezia::config_key::underloadPacketMagicHeader, wgConfig.value(amnezia::config_key::underloadPacketMagicHeader));
|
json.insert(amnezia::config_key::underloadPacketMagicHeader, wgConfig.value(amnezia::config_key::underloadPacketMagicHeader));
|
||||||
json.insert(amnezia::config_key::transportPacketMagicHeader, wgConfig.value(amnezia::config_key::transportPacketMagicHeader));
|
json.insert(amnezia::config_key::transportPacketMagicHeader, wgConfig.value(amnezia::config_key::transportPacketMagicHeader));
|
||||||
// json.insert(amnezia::config_key::specialJunk1, wgConfig.value(amnezia::config_key::specialJunk1));
|
json.insert(amnezia::config_key::specialJunk1, wgConfig.value(amnezia::config_key::specialJunk1));
|
||||||
// json.insert(amnezia::config_key::specialJunk2, wgConfig.value(amnezia::config_key::specialJunk2));
|
json.insert(amnezia::config_key::specialJunk2, wgConfig.value(amnezia::config_key::specialJunk2));
|
||||||
// json.insert(amnezia::config_key::specialJunk3, wgConfig.value(amnezia::config_key::specialJunk3));
|
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::specialJunk4, wgConfig.value(amnezia::config_key::specialJunk4));
|
||||||
// json.insert(amnezia::config_key::specialJunk5, wgConfig.value(amnezia::config_key::specialJunk5));
|
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);
|
write(json);
|
||||||
@@ -449,6 +437,7 @@ void LocalSocketController::parseCommand(const QByteArray& command) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (type == "status") {
|
if (type == "status") {
|
||||||
|
|
||||||
QJsonValue serverIpv4Gateway = obj.value("serverIpv4Gateway");
|
QJsonValue serverIpv4Gateway = obj.value("serverIpv4Gateway");
|
||||||
if (!serverIpv4Gateway.isString()) {
|
if (!serverIpv4Gateway.isString()) {
|
||||||
logger.error() << "Unexpected serverIpv4Gateway value";
|
logger.error() << "Unexpected serverIpv4Gateway value";
|
||||||
@@ -493,6 +482,11 @@ void LocalSocketController::parseCommand(const QByteArray& command) {
|
|||||||
|
|
||||||
logger.debug() << "Handshake completed with:"
|
logger.debug() << "Handshake completed with:"
|
||||||
<< pubkey.toString();
|
<< pubkey.toString();
|
||||||
|
|
||||||
|
checkStatus();
|
||||||
|
|
||||||
|
emit statusUpdated("", m_deviceIpv4, 0, 0);
|
||||||
|
|
||||||
emit connected(pubkey.toString());
|
emit connected(pubkey.toString());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
#include "controllerimpl.h"
|
#include "controllerimpl.h"
|
||||||
|
|
||||||
|
|
||||||
class QJsonObject;
|
class QJsonObject;
|
||||||
|
|
||||||
class LocalSocketController final : public ControllerImpl {
|
class LocalSocketController final : public ControllerImpl {
|
||||||
@@ -58,6 +59,7 @@ class LocalSocketController final : public ControllerImpl {
|
|||||||
|
|
||||||
QByteArray m_buffer;
|
QByteArray m_buffer;
|
||||||
|
|
||||||
|
QString m_deviceIpv4;
|
||||||
std::function<void(const QString&)> m_logCallback = nullptr;
|
std::function<void(const QString&)> m_logCallback = nullptr;
|
||||||
|
|
||||||
QTimer m_initializingTimer;
|
QTimer m_initializingTimer;
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
//#include "mozillavpn.h"
|
//#include "mozillavpn.h"
|
||||||
#include "networkwatcherimpl.h"
|
#include "networkwatcherimpl.h"
|
||||||
#include "platforms/dummy/dummynetworkwatcher.h"
|
|
||||||
//#include "settingsholder.h"
|
//#include "settingsholder.h"
|
||||||
|
|
||||||
#ifdef MZ_WINDOWS
|
#ifdef MZ_WINDOWS
|
||||||
@@ -51,7 +50,7 @@ NetworkWatcher::NetworkWatcher() { MZ_COUNT_CTOR(NetworkWatcher); }
|
|||||||
NetworkWatcher::~NetworkWatcher() { MZ_COUNT_DTOR(NetworkWatcher); }
|
NetworkWatcher::~NetworkWatcher() { MZ_COUNT_DTOR(NetworkWatcher); }
|
||||||
|
|
||||||
void NetworkWatcher::initialize() {
|
void NetworkWatcher::initialize() {
|
||||||
logger.debug() << "Initialize";
|
logger.debug() << "Initialize NetworkWatcher";
|
||||||
|
|
||||||
#if defined(MZ_WINDOWS)
|
#if defined(MZ_WINDOWS)
|
||||||
m_impl = new WindowsNetworkWatcher(this);
|
m_impl = new WindowsNetworkWatcher(this);
|
||||||
@@ -69,59 +68,39 @@ void NetworkWatcher::initialize() {
|
|||||||
m_impl = new DummyNetworkWatcher(this);
|
m_impl = new DummyNetworkWatcher(this);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
connect(m_impl, &NetworkWatcherImpl::unsecuredNetwork, this,
|
connect(m_impl, &NetworkWatcherImpl::unsecuredNetwork, this,
|
||||||
&NetworkWatcher::unsecuredNetwork);
|
&NetworkWatcher::unsecuredNetwork);
|
||||||
connect(m_impl, &NetworkWatcherImpl::networkChanged, this,
|
connect(m_impl, &NetworkWatcherImpl::networkChanged, this,
|
||||||
&NetworkWatcher::networkChange);
|
&NetworkWatcher::networkChanged);
|
||||||
|
connect(m_impl, &NetworkWatcherImpl::wakeup, this,
|
||||||
|
&NetworkWatcher::wakeup);
|
||||||
m_impl->initialize();
|
m_impl->initialize();
|
||||||
|
|
||||||
|
// Enable sleep/wake monitoring for VPN auto-reconnection
|
||||||
// TODO: IMPL FOR AMNEZIA
|
logger.debug() << "Starting NetworkWatcher for sleep/wake monitoring";
|
||||||
#if 0
|
logger.debug() << "About to call m_impl->start()";
|
||||||
SettingsHolder* settingsHolder = SettingsHolder::instance();
|
try {
|
||||||
Q_ASSERT(settingsHolder);
|
|
||||||
|
|
||||||
m_active = settingsHolder->unsecuredNetworkAlert() ||
|
|
||||||
settingsHolder->captivePortalAlert();
|
|
||||||
m_reportUnsecuredNetwork = settingsHolder->unsecuredNetworkAlert();
|
|
||||||
if (m_active) {
|
|
||||||
m_impl->start();
|
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()";
|
||||||
}
|
}
|
||||||
|
m_active = true;
|
||||||
connect(settingsHolder, &SettingsHolder::unsecuredNetworkAlertChanged, this,
|
m_reportUnsecuredNetwork = false; // Disable unsecured network alerts for Amnezia
|
||||||
&NetworkWatcher::settingsChanged);
|
|
||||||
connect(settingsHolder, &SettingsHolder::captivePortalAlertChanged, this,
|
|
||||||
&NetworkWatcher::settingsChanged);
|
|
||||||
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void NetworkWatcher::settingsChanged() {
|
void NetworkWatcher::settingsChanged() {
|
||||||
// TODO: IMPL FOR AMNEZIA
|
// For Amnezia: Keep NetworkWatcher always active for sleep/wake monitoring
|
||||||
#if 0
|
logger.debug() << "NetworkWatcher settings changed - keeping sleep monitoring active";
|
||||||
SettingsHolder* settingsHolder = SettingsHolder::instance();
|
|
||||||
m_active = settingsHolder->unsecuredNetworkAlert() ||
|
|
||||||
settingsHolder->captivePortalAlert();
|
|
||||||
m_reportUnsecuredNetwork = settingsHolder->unsecuredNetworkAlert();
|
|
||||||
|
|
||||||
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::unsecuredNetwork(const QString& networkName,
|
void NetworkWatcher::unsecuredNetwork(const QString& networkName,
|
||||||
const QString& networkId) {
|
const QString& networkId) {
|
||||||
logger.debug() << "Unsecured network:" << logger.sensitive(networkName)
|
logger.debug() << "Unsecured network:" << logger.sensitive(networkName)
|
||||||
<< "id:" << logger.sensitive(networkId);
|
<< "id:" << logger.sensitive(networkId);
|
||||||
|
|
||||||
#ifndef UNIT_TEST
|
#ifndef UNIT_TEST
|
||||||
if (!m_reportUnsecuredNetwork) {
|
if (!m_reportUnsecuredNetwork) {
|
||||||
logger.debug() << "Disabled. Ignoring unsecured network";
|
logger.debug() << "Disabled. Ignoring unsecured network";
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ public:
|
|||||||
QNetworkInformation::Reachability getReachability();
|
QNetworkInformation::Reachability getReachability();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void networkChange();
|
void networkChanged();
|
||||||
|
void wakeup();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void settingsChanged();
|
void settingsChanged();
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ signals:
|
|||||||
// TODO: Only windows-networkwatcher has this, the other plattforms should
|
// TODO: Only windows-networkwatcher has this, the other plattforms should
|
||||||
// too.
|
// too.
|
||||||
void networkChanged(QString newBSSID);
|
void networkChanged(QString newBSSID);
|
||||||
|
void wakeup();
|
||||||
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool m_active = false;
|
bool m_active = false;
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ void PingHelper::start(const QString& serverIpv4Gateway,
|
|||||||
|
|
||||||
m_gateway = QHostAddress(serverIpv4Gateway);
|
m_gateway = QHostAddress(serverIpv4Gateway);
|
||||||
m_source = QHostAddress(deviceIpv4Address.section('/', 0, 0));
|
m_source = QHostAddress(deviceIpv4Address.section('/', 0, 0));
|
||||||
|
|
||||||
m_pingSender = PingSenderFactory::create(m_source, this);
|
m_pingSender = PingSenderFactory::create(m_source, this);
|
||||||
|
|
||||||
// Some platforms require root access to send and receive ICMP pings. If
|
// 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,
|
connect(m_pingSender, &PingSender::recvPing, this, &PingHelper::pingReceived,
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
connect(m_pingSender, &PingSender::criticalPingError, this,
|
connect(m_pingSender, &PingSender::criticalPingError, this, [this]() {
|
||||||
[]() { logger.info() << "Encountered Unrecoverable ping error"; });
|
logger.info() << "Encountered Unrecoverable ping error";
|
||||||
|
emit connectionLose();
|
||||||
|
});
|
||||||
|
|
||||||
// Reset the ping statistics
|
// Reset the ping statistics
|
||||||
m_sequence = 0;
|
m_sequence = 0;
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ class PingHelper final : public QObject {
|
|||||||
|
|
||||||
signals:
|
signals:
|
||||||
void pingSentAndReceived(qint64 msec);
|
void pingSentAndReceived(qint64 msec);
|
||||||
|
void connectionLose();
|
||||||
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void nextPing();
|
void nextPing();
|
||||||
|
|||||||
@@ -5,27 +5,26 @@
|
|||||||
#include "pingsenderfactory.h"
|
#include "pingsenderfactory.h"
|
||||||
|
|
||||||
#if defined(MZ_LINUX) || defined(MZ_ANDROID)
|
#if defined(MZ_LINUX) || defined(MZ_ANDROID)
|
||||||
//# include "platforms/linux/linuxpingsender.h"
|
# include "platforms/linux/linuxpingsender.h"
|
||||||
#elif defined(MZ_MACOS) || defined(MZ_IOS)
|
#elif defined(MZ_MACOS) || defined(MZ_IOS)
|
||||||
# include "platforms/macos/macospingsender.h"
|
# include "platforms/macos/macospingsender.h"
|
||||||
#elif defined(MZ_WINDOWS)
|
#elif defined(MZ_WINDOWS)
|
||||||
# include "platforms/windows/windowspingsender.h"
|
# include "platforms/windows/windowspingsender.h"
|
||||||
#elif defined(MZ_DUMMY) || defined(UNIT_TEST)
|
#elif defined(MZ_WASM) || defined(UNIT_TEST)
|
||||||
# include "platforms/dummy/dummypingsender.h"
|
# include "platforms/dummy/dummypingsender.h"
|
||||||
#else
|
#else
|
||||||
# error "Unsupported platform"
|
# error "Unsupported platform"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
PingSender* PingSenderFactory::create(const QHostAddress& source,
|
PingSender* PingSenderFactory::create(const QHostAddress& source,
|
||||||
QObject* parent) {
|
QObject* parent) {
|
||||||
#if defined(MZ_LINUX) || defined(MZ_ANDROID)
|
#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)
|
#elif defined(MZ_MACOS) || defined(MZ_IOS)
|
||||||
return new MacOSPingSender(source, parent);
|
return new MacOSPingSender(source, parent);
|
||||||
#elif defined(MZ_WINDOWS)
|
#elif defined(MZ_WINDOWS)
|
||||||
return new WindowsPingSender(source, parent);
|
return new WindowsPingSender(source, parent);
|
||||||
#else
|
#else
|
||||||
return new DummyPingSender(source, parent);
|
return new DummyPingSender(source, parent);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ class QHostAddress;
|
|||||||
class QObject;
|
class QObject;
|
||||||
|
|
||||||
class PingSenderFactory final {
|
class PingSenderFactory final {
|
||||||
public:
|
public:
|
||||||
PingSenderFactory() = delete;
|
PingSenderFactory() = delete;
|
||||||
static PingSender* create(const QHostAddress& source, QObject* parent);
|
static PingSender* create(const QHostAddress& source, QObject* parent);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
#endif // PINGSENDERFACTORY_H
|
#endif // PINGSENDERFACTORY_H
|
||||||
|
|||||||
@@ -99,7 +99,11 @@ bool AndroidController::initialize()
|
|||||||
{"onFileOpened", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onFileOpened)},
|
{"onFileOpened", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onFileOpened)},
|
||||||
{"onConfigImported", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onConfigImported)},
|
{"onConfigImported", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onConfigImported)},
|
||||||
{"onAuthResult", "(Z)V", reinterpret_cast<void *>(onAuthResult)},
|
{"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)},
|
||||||
|
{"onActivityPaused", "()V", reinterpret_cast<void *>(onActivityPaused)},
|
||||||
|
{"onActivityResumed", "()V", reinterpret_cast<void *>(onActivityResumed)}
|
||||||
};
|
};
|
||||||
|
|
||||||
QJniEnvironment env;
|
QJniEnvironment env;
|
||||||
@@ -202,6 +206,21 @@ bool AndroidController::isOnTv()
|
|||||||
return callActivityMethod<jboolean>("isOnTv", "()Z");
|
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()
|
void AndroidController::startQrReaderActivity()
|
||||||
{
|
{
|
||||||
callActivityMethod("startQrCodeReader", "()V");
|
callActivityMethod("startQrCodeReader", "()V");
|
||||||
@@ -521,3 +540,42 @@ bool AndroidController::decodeQrCode(JNIEnv *env, jobject thiz, jstring data)
|
|||||||
|
|
||||||
return ImportController::decodeQrCode(AndroidUtils::convertJString(env, 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// static
|
||||||
|
void AndroidController::onActivityPaused(JNIEnv *env, jobject thiz)
|
||||||
|
{
|
||||||
|
Q_UNUSED(env);
|
||||||
|
Q_UNUSED(thiz);
|
||||||
|
|
||||||
|
emit AndroidController::instance()->activityPaused();
|
||||||
|
}
|
||||||
|
|
||||||
|
// static
|
||||||
|
void AndroidController::onActivityResumed(JNIEnv *env, jobject thiz)
|
||||||
|
{
|
||||||
|
Q_UNUSED(env);
|
||||||
|
Q_UNUSED(thiz);
|
||||||
|
|
||||||
|
emit AndroidController::instance()->activityResumed();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ public:
|
|||||||
QString getFileName(const QString &uri);
|
QString getFileName(const QString &uri);
|
||||||
bool isCameraPresent();
|
bool isCameraPresent();
|
||||||
bool isOnTv();
|
bool isOnTv();
|
||||||
|
bool isEdgeToEdgeEnabled();
|
||||||
|
int getStatusBarHeight();
|
||||||
|
int getNavigationBarHeight();
|
||||||
void startQrReaderActivity();
|
void startQrReaderActivity();
|
||||||
void setSaveLogs(bool enabled);
|
void setSaveLogs(bool enabled);
|
||||||
void exportLogsFile(const QString &fileName);
|
void exportLogsFile(const QString &fileName);
|
||||||
@@ -70,6 +73,10 @@ signals:
|
|||||||
void importConfigFromOutside(QString config);
|
void importConfigFromOutside(QString config);
|
||||||
void initConnectionState(Vpn::ConnectionState state);
|
void initConnectionState(Vpn::ConnectionState state);
|
||||||
void authenticationResult(bool result);
|
void authenticationResult(bool result);
|
||||||
|
void imeInsetsChanged(int heightDp);
|
||||||
|
void systemBarsInsetsChanged(int navBarHeightDp, int statusBarHeightDp);
|
||||||
|
void activityPaused();
|
||||||
|
void activityResumed();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool isWaitingStatus = true;
|
bool isWaitingStatus = true;
|
||||||
@@ -98,6 +105,10 @@ private:
|
|||||||
static void onFileOpened(JNIEnv *env, jobject thiz, jstring uri);
|
static void onFileOpened(JNIEnv *env, jobject thiz, jstring uri);
|
||||||
static void onAuthResult(JNIEnv *env, jobject thiz, jboolean result);
|
static void onAuthResult(JNIEnv *env, jobject thiz, jboolean result);
|
||||||
static bool decodeQrCode(JNIEnv *env, jobject thiz, jstring data);
|
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);
|
||||||
|
static void onActivityPaused(JNIEnv *env, jobject thiz);
|
||||||
|
static void onActivityResumed(JNIEnv *env, jobject thiz);
|
||||||
|
|
||||||
template <typename Ret, typename ...Args>
|
template <typename Ret, typename ...Args>
|
||||||
static auto callActivityMethod(const char *methodName, const char *signature, Args &&...args);
|
static auto callActivityMethod(const char *methodName, const char *signature, Args &&...args);
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import Foundation
|
|||||||
import os.log
|
import os.log
|
||||||
|
|
||||||
struct 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"
|
private static let IsLoggingEnabledKey = "IsLoggingEnabled"
|
||||||
static var isLoggingEnabled: Bool {
|
static var isLoggingEnabled: Bool {
|
||||||
@@ -77,9 +78,40 @@ struct Log {
|
|||||||
static func log(_ type: OSLogType, title: String = "", message: String, url: URL = neLogURL) {
|
static func log(_ type: OSLogType, title: String = "", message: String, url: URL = neLogURL) {
|
||||||
NSLog("\(title) \(message)")
|
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 date = Date()
|
||||||
let level = Record.Level(from: type)
|
let level = Record.Level(from: type)
|
||||||
|
|||||||
@@ -1,22 +1,76 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import os.log
|
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) {
|
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) {
|
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)
|
neLog(type, title: "WG: \(title)", message: message)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func ovpnLog(_ type: OSLogType, title: String = "", message: String) {
|
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)
|
neLog(type, title: "OVPN: \(title)", message: message)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func xrayLog(_ type: OSLogType, title: String = "", message: String) {
|
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)
|
neLog(type, title: "XRAY: \(title)", message: message)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func neLog(_ type: OSLogType, title: String = "", message: String) {
|
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)
|
Log.log(type, title: "NE: \(title)", message: message)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import NetworkExtension
|
import NetworkExtension
|
||||||
import OpenVPNAdapter
|
import OpenVPNAdapter
|
||||||
|
import CryptoKit
|
||||||
|
|
||||||
struct OpenVPNConfig: Decodable {
|
struct OpenVPNConfig: Decodable {
|
||||||
let config: String
|
let config: String
|
||||||
@@ -14,6 +15,12 @@ struct OpenVPNConfig: Decodable {
|
|||||||
|
|
||||||
extension PacketTunnelProvider {
|
extension PacketTunnelProvider {
|
||||||
func startOpenVPN(completionHandler: @escaping (Error?) -> Void) {
|
func startOpenVPN(completionHandler: @escaping (Error?) -> Void) {
|
||||||
|
// Reset session-derived state so reconnects never reuse stale gateway/address data.
|
||||||
|
openVpnGatewayAddress = nil
|
||||||
|
openVpnLocalAddress = nil
|
||||||
|
openVpnLocalMask = nil
|
||||||
|
lastOpenVPNSettings = nil
|
||||||
|
|
||||||
guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol,
|
guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol,
|
||||||
let providerConfiguration = protocolConfiguration.providerConfiguration,
|
let providerConfiguration = protocolConfiguration.providerConfiguration,
|
||||||
let openVPNConfigData = providerConfiguration[Constants.ovpnConfigKey] as? Data else {
|
let openVPNConfigData = providerConfiguration[Constants.ovpnConfigKey] as? Data else {
|
||||||
@@ -24,29 +31,180 @@ extension PacketTunnelProvider {
|
|||||||
do {
|
do {
|
||||||
let openVPNConfig = try JSONDecoder().decode(OpenVPNConfig.self, from: openVPNConfigData)
|
let openVPNConfig = try JSONDecoder().decode(OpenVPNConfig.self, from: openVPNConfigData)
|
||||||
ovpnLog(.info, title: "config: ", message: openVPNConfig.str)
|
ovpnLog(.info, title: "config: ", message: openVPNConfig.str)
|
||||||
|
let wrapperPreview = String(decoding: openVPNConfigData.prefix(512), as: UTF8.self)
|
||||||
|
let ovpnPreview = String(openVPNConfig.config.prefix(512))
|
||||||
|
ovpnLog(.info, title: "config wrapper", message: "bytes=\(openVPNConfigData.count) preview=\(wrapperPreview)")
|
||||||
|
ovpnLog(.info, title: "config raw", message: "chars=\(openVPNConfig.config.count) preview=\(ovpnPreview)")
|
||||||
let ovpnConfiguration = Data(openVPNConfig.config.utf8)
|
let ovpnConfiguration = Data(openVPNConfig.config.utf8)
|
||||||
|
splitTunnelType = openVPNConfig.splitTunnelType
|
||||||
|
splitTunnelSites = openVPNConfig.splitTunnelSites
|
||||||
|
openVpnDnsServers = Self.extractDnsServers(from: openVPNConfig.config)
|
||||||
|
openVpnRemoteAddress = Self.extractRemoteHost(from: openVPNConfig.config)
|
||||||
|
openVpnRedirectGatewayDef1 = Self.hasRedirectGatewayDef1(in: openVPNConfig.config)
|
||||||
|
if let openVpnRemoteAddress {
|
||||||
|
ovpnLog(.info, title: "Remote", message: "host=\(openVpnRemoteAddress)")
|
||||||
|
}
|
||||||
|
if !openVpnDnsServers.isEmpty {
|
||||||
|
ovpnLog(.info, title: "DNS", message: "servers=\(openVpnDnsServers)")
|
||||||
|
}
|
||||||
|
if openVpnRedirectGatewayDef1 {
|
||||||
|
ovpnLog(.info, title: "IPv4Routes", message: "redirect-gateway def1 detected")
|
||||||
|
}
|
||||||
setupAndlaunchOpenVPN(withConfig: ovpnConfiguration, completionHandler: completionHandler)
|
setupAndlaunchOpenVPN(withConfig: ovpnConfiguration, completionHandler: completionHandler)
|
||||||
} catch {
|
} catch {
|
||||||
ovpnLog(.error, message: "Can't parse config: \(error.localizedDescription)")
|
ovpnLog(.error, message: "Can't parse OpenVPN config: \(error.localizedDescription)")
|
||||||
|
|
||||||
if let underlyingError = (error as NSError).userInfo[NSUnderlyingErrorKey] as? NSError {
|
|
||||||
ovpnLog(.error, message: "Can't parse config: \(underlyingError.localizedDescription)")
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
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,
|
private func setupAndlaunchOpenVPN(withConfig ovpnConfiguration: Data,
|
||||||
withShadowSocks viaSS: Bool = false,
|
withShadowSocks viaSS: Bool = false,
|
||||||
completionHandler: @escaping (Error?) -> Void) {
|
completionHandler: @escaping (Error?) -> Void) {
|
||||||
ovpnLog(.info, message: "Setup and launch")
|
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 hasCertTag = configString.contains("<cert>") && configString.contains("</cert>")
|
||||||
|
let hasKeyTag = configString.contains("<key>") && configString.contains("</key>")
|
||||||
|
let hasAuthUserPass = configString.contains("auth-user-pass")
|
||||||
|
ovpnLog(.info, title: "ConfigCreds", message: "inlineCert=\(hasCertTag) inlineKey=\(hasKeyTag) authUserPass=\(hasAuthUserPass)")
|
||||||
|
|
||||||
|
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 hasTlsAuthOpen && hasTlsAuthClose {
|
||||||
|
ovpnLog(.info, title: "TLSAuthSanitized", message: "preserve original tls-auth block")
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedConfig = configString.replacingOccurrences(of: "\r\n", with: "\n")
|
||||||
|
normalizedConfig = Self.normalizeInlineBlock(
|
||||||
|
in: normalizedConfig,
|
||||||
|
tag: "ca",
|
||||||
|
beginMarkers: ["-----BEGIN CERTIFICATE-----"],
|
||||||
|
endMarkers: ["-----END CERTIFICATE-----"]
|
||||||
|
)
|
||||||
|
normalizedConfig = Self.normalizeInlineBlock(
|
||||||
|
in: normalizedConfig,
|
||||||
|
tag: "cert",
|
||||||
|
beginMarkers: ["-----BEGIN CERTIFICATE-----"],
|
||||||
|
endMarkers: ["-----END CERTIFICATE-----"]
|
||||||
|
)
|
||||||
|
normalizedConfig = Self.normalizeInlineBlock(
|
||||||
|
in: normalizedConfig,
|
||||||
|
tag: "key",
|
||||||
|
beginMarkers: [
|
||||||
|
"-----BEGIN PRIVATE KEY-----",
|
||||||
|
"-----BEGIN RSA PRIVATE KEY-----",
|
||||||
|
"-----BEGIN EC PRIVATE KEY-----",
|
||||||
|
"-----BEGIN ENCRYPTED PRIVATE KEY-----"
|
||||||
|
],
|
||||||
|
endMarkers: [
|
||||||
|
"-----END PRIVATE KEY-----",
|
||||||
|
"-----END RSA PRIVATE KEY-----",
|
||||||
|
"-----END EC PRIVATE KEY-----",
|
||||||
|
"-----END ENCRYPTED PRIVATE KEY-----"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
normalizedConfig = Self.normalizeInlineBlock(
|
||||||
|
in: normalizedConfig,
|
||||||
|
tag: "tls-auth",
|
||||||
|
beginMarkers: ["-----BEGIN OpenVPN Static key V1-----"],
|
||||||
|
endMarkers: ["-----END OpenVPN Static key V1-----"]
|
||||||
|
)
|
||||||
|
normalizedConfig = Self.stripUnsupportedOptions(forOpenVPNAdapter: normalizedConfig)
|
||||||
|
if !normalizedConfig.hasSuffix("\n") {
|
||||||
|
normalizedConfig.append("\n")
|
||||||
|
}
|
||||||
|
let normalizedLines = normalizedConfig.split(whereSeparator: \.isNewline)
|
||||||
|
let normalizedTail = normalizedLines.suffix(10).joined(separator: "\n")
|
||||||
|
ovpnLog(.debug, title: "ConfigTailSanitized", message: normalizedTail)
|
||||||
|
let redirectLines = normalizedLines
|
||||||
|
.map(String.init)
|
||||||
|
.filter { $0.lowercased().contains("redirect-gateway") }
|
||||||
|
if !redirectLines.isEmpty {
|
||||||
|
ovpnLog(.info, title: "ConfigRedirect", message: redirectLines.joined(separator: " | "))
|
||||||
|
}
|
||||||
|
let controlScalars = normalizedConfig.unicodeScalars.filter {
|
||||||
|
($0.value < 0x20 && $0 != "\n" && $0 != "\r" && $0 != "\t")
|
||||||
|
}
|
||||||
|
if !controlScalars.isEmpty {
|
||||||
|
ovpnLog(.error, title: "ConfigChars", message: "nonPrintableControlCount=\(controlScalars.count)")
|
||||||
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
let dumpBaseURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
|
||||||
|
?? FileManager.default.temporaryDirectory
|
||||||
|
let dumpURL = dumpBaseURL.appendingPathComponent("amnezia_ovpn_adapter_config.conf")
|
||||||
|
do {
|
||||||
|
try normalizedConfig.write(to: dumpURL, atomically: true, encoding: .utf8)
|
||||||
|
ovpnLog(.info, title: "ConfigDump", message: "path=\(dumpURL.path) bytes=\(normalizedConfig.utf8.count)")
|
||||||
|
} catch {
|
||||||
|
ovpnLog(.error, title: "ConfigDump", message: "write failed: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
let sanitizedData = Data(normalizedConfig.utf8)
|
||||||
|
|
||||||
let configuration = OpenVPNConfiguration()
|
let configuration = OpenVPNConfiguration()
|
||||||
configuration.fileContent = ovpnConfiguration
|
configuration.fileContent = sanitizedData
|
||||||
if str.contains("cloak") {
|
// Be explicit: enum default is 0 (enabled), we need stubs-only behavior.
|
||||||
|
configuration.compressionMode = .disabled
|
||||||
|
// A-012: emulate OpenVPN2 CLI capability advertisement as closely as possible.
|
||||||
|
configuration.peerInfo = [
|
||||||
|
"IV_VER": "2.6.10",
|
||||||
|
"IV_PLAT": "mac",
|
||||||
|
"IV_TCPNL": "1",
|
||||||
|
"IV_MTU": "1600",
|
||||||
|
"IV_NCP": "2",
|
||||||
|
"IV_CIPHERS": "AES-256-GCM:AES-128-GCM:CHACHA20-POLY1305",
|
||||||
|
"IV_PROTO": "990",
|
||||||
|
"IV_LZO_STUB": "1",
|
||||||
|
"IV_COMP_STUB": "1",
|
||||||
|
"IV_COMP_STUBv2": "1"
|
||||||
|
]
|
||||||
|
if let peerInfo = configuration.peerInfo {
|
||||||
|
let peerInfoSummary = peerInfo.keys.sorted().map { "\($0)=\(peerInfo[$0] ?? "")" }.joined(separator: " ")
|
||||||
|
ovpnLog(.info, title: "PeerInfoOverride", message: peerInfoSummary)
|
||||||
|
}
|
||||||
|
if configString.contains("cloak") {
|
||||||
configuration.setPTCloak()
|
configuration.setPTCloak()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +215,8 @@ extension PacketTunnelProvider {
|
|||||||
evaluation = try ovpnAdapter?.apply(configuration: configuration)
|
evaluation = try ovpnAdapter?.apply(configuration: configuration)
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
|
let nsError = error as NSError
|
||||||
|
ovpnLog(.error, title: "ApplyConfig", message: "domain=\(nsError.domain) code=\(nsError.code) info=\(nsError.userInfo)")
|
||||||
completionHandler(error)
|
completionHandler(error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -64,14 +224,18 @@ extension PacketTunnelProvider {
|
|||||||
if evaluation?.autologin == false {
|
if evaluation?.autologin == false {
|
||||||
ovpnLog(.info, message: "Implement login with user credentials")
|
ovpnLog(.info, message: "Implement login with user credentials")
|
||||||
}
|
}
|
||||||
|
if let evaluation {
|
||||||
vpnReachability.startTracking { [weak self] status in
|
ovpnLog(.info, title: "ConfigEval", message: "autologin=\(evaluation.autologin) externalPki=\(evaluation.externalPki)")
|
||||||
guard status == .reachableViaWiFi else { return }
|
|
||||||
self?.ovpnAdapter?.reconnect(afterTimeInterval: 5)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !os(macOS)
|
||||||
|
vpnReachability.startTracking { [weak self] status in
|
||||||
|
self?.handleOpenVPNReachabilityChange(status)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
startHandler = completionHandler
|
startHandler = completionHandler
|
||||||
ovpnAdapter?.connect(using: packetFlow)
|
ovpnAdapter?.connect(using: openVPNPacketFlow())
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleOpenVPNStatusMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
|
func handleOpenVPNStatusMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
|
||||||
@@ -84,6 +248,8 @@ extension PacketTunnelProvider {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ovpnLog(.info, title: "Transport", message: "bytesIn=\(bytesin) bytesOut=\(bytesout)")
|
||||||
|
|
||||||
let response: [String: Any] = [
|
let response: [String: Any] = [
|
||||||
"rx_bytes": bytesin,
|
"rx_bytes": bytesin,
|
||||||
"tx_bytes": bytesout
|
"tx_bytes": bytesout
|
||||||
@@ -93,9 +259,13 @@ extension PacketTunnelProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func stopOpenVPN(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
|
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
|
stopHandler = completionHandler
|
||||||
|
openVpnGatewayAddress = nil
|
||||||
|
openVpnLocalAddress = nil
|
||||||
|
openVpnLocalMask = nil
|
||||||
|
lastOpenVPNSettings = nil
|
||||||
if vpnReachability.isTracking {
|
if vpnReachability.isTracking {
|
||||||
vpnReachability.stopTracking()
|
vpnReachability.stopTracking()
|
||||||
}
|
}
|
||||||
@@ -115,11 +285,99 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
|||||||
configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?,
|
configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?,
|
||||||
completionHandler: @escaping (Error?) -> Void
|
completionHandler: @escaping (Error?) -> Void
|
||||||
) {
|
) {
|
||||||
|
guard var effectiveSettings = networkSettings else {
|
||||||
|
ovpnLog(.info, title: "SetTunnelNetworkSettings", message: "nil settings; skipping update")
|
||||||
|
completionHandler(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let splitType = splitTunnelType ?? 0
|
||||||
|
|
||||||
|
if let ipv4Settings = effectiveSettings.ipv4Settings {
|
||||||
|
openVpnLocalAddress = ipv4Settings.addresses.first
|
||||||
|
openVpnLocalMask = ipv4Settings.subnetMasks.first
|
||||||
|
}
|
||||||
|
|
||||||
|
let serverIP = openVPNAdapter.connectionInformation?.serverIP
|
||||||
|
let configRemote = openVpnRemoteAddress
|
||||||
|
let serverEndpoint: String? = {
|
||||||
|
if let ip = serverIP, Self.isIPv4Address(ip) { return ip }
|
||||||
|
if let ip = configRemote, Self.isIPv4Address(ip) { return ip }
|
||||||
|
return effectiveSettings.tunnelRemoteAddress
|
||||||
|
}()
|
||||||
|
|
||||||
|
if let serverEndpoint,
|
||||||
|
Self.isIPv4Address(serverEndpoint),
|
||||||
|
effectiveSettings.tunnelRemoteAddress != serverEndpoint {
|
||||||
|
let updatedSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: serverEndpoint)
|
||||||
|
updatedSettings.ipv4Settings = effectiveSettings.ipv4Settings
|
||||||
|
updatedSettings.ipv6Settings = effectiveSettings.ipv6Settings
|
||||||
|
updatedSettings.dnsSettings = effectiveSettings.dnsSettings
|
||||||
|
updatedSettings.proxySettings = effectiveSettings.proxySettings
|
||||||
|
updatedSettings.mtu = effectiveSettings.mtu
|
||||||
|
effectiveSettings = updatedSettings
|
||||||
|
ovpnLog(.info, title: "Remote", message: "tunnelRemoteAddress set to server=\(serverEndpoint)")
|
||||||
|
} else if let serverEndpoint, !Self.isIPv4Address(serverEndpoint) {
|
||||||
|
ovpnLog(.info, title: "Remote", message: "skip tunnelRemoteAddress override; non-ip serverEndpoint=\(serverEndpoint)")
|
||||||
|
}
|
||||||
|
|
||||||
// In order to direct all DNS queries first to the VPN DNS servers before the primary DNS servers
|
// In order to direct all DNS queries first to the VPN DNS servers before the primary DNS servers
|
||||||
// send empty string to NEDNSSettings.matchDomains
|
// send empty string to NEDNSSettings.matchDomains
|
||||||
networkSettings?.dnsSettings?.matchDomains = [""]
|
if let dnsSettings = effectiveSettings.dnsSettings {
|
||||||
|
if dnsSettings.servers.isEmpty, !openVpnDnsServers.isEmpty {
|
||||||
|
let newSettings = NEDNSSettings(servers: openVpnDnsServers)
|
||||||
|
newSettings.matchDomains = dnsSettings.matchDomains
|
||||||
|
effectiveSettings.dnsSettings = newSettings
|
||||||
|
}
|
||||||
|
} else if !openVpnDnsServers.isEmpty {
|
||||||
|
let newSettings = NEDNSSettings(servers: openVpnDnsServers)
|
||||||
|
effectiveSettings.dnsSettings = newSettings
|
||||||
|
}
|
||||||
|
|
||||||
if splitTunnelType == 1 {
|
effectiveSettings.dnsSettings?.matchDomains = [""]
|
||||||
|
if let dnsSettings = effectiveSettings.dnsSettings {
|
||||||
|
let servers = dnsSettings.servers.joined(separator: ",")
|
||||||
|
let domains = dnsSettings.matchDomains?.joined(separator: ",") ?? ""
|
||||||
|
ovpnLog(.info, title: "DNS", message: "servers=[\(servers)] matchDomains=[\(domains)]")
|
||||||
|
} else {
|
||||||
|
ovpnLog(.error, title: "DNS", message: "dnsSettings is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
let tunnelRemote = effectiveSettings.tunnelRemoteAddress
|
||||||
|
if !tunnelRemote.isEmpty {
|
||||||
|
ovpnLog(.info, title: "Remote", message: "tunnelRemoteAddress=\(tunnelRemote)")
|
||||||
|
} else if let remoteAddress = openVpnRemoteAddress {
|
||||||
|
ovpnLog(.info, title: "Remote", message: "tunnelRemoteAddress is empty, configRemote=\(remoteAddress)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if let ipv4Settings = effectiveSettings.ipv4Settings {
|
||||||
|
let included = (ipv4Settings.includedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationSubnetMask)" }
|
||||||
|
let excluded = (ipv4Settings.excludedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationSubnetMask)" }
|
||||||
|
let addresses = ipv4Settings.addresses.joined(separator: ",")
|
||||||
|
let masks = ipv4Settings.subnetMasks.joined(separator: ",")
|
||||||
|
let router: String
|
||||||
|
#if os(macOS)
|
||||||
|
if #available(macOS 13.0, *) {
|
||||||
|
router = ipv4Settings.router ?? ""
|
||||||
|
} else {
|
||||||
|
router = ""
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
router = ""
|
||||||
|
#endif
|
||||||
|
ovpnLog(.info, title: "IPv4RoutesPre", message: "addresses=[\(addresses)] masks=[\(masks)] router=\(router) included=\(included) excluded=\(excluded)")
|
||||||
|
} else {
|
||||||
|
ovpnLog(.error, title: "IPv4RoutesPre", message: "ipv4Settings is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if let ipv6Settings = effectiveSettings.ipv6Settings {
|
||||||
|
let included = (ipv6Settings.includedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationNetworkPrefixLength)" }
|
||||||
|
let excluded = (ipv6Settings.excludedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationNetworkPrefixLength)" }
|
||||||
|
let addresses = ipv6Settings.addresses.joined(separator: ",")
|
||||||
|
let prefixes = ipv6Settings.networkPrefixLengths.map { "\($0)" }.joined(separator: ",")
|
||||||
|
ovpnLog(.info, title: "IPv6RoutesPre", message: "addresses=[\(addresses)] prefixes=[\(prefixes)] included=\(included) excluded=\(excluded)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if splitType == 1 {
|
||||||
var ipv4IncludedRoutes = [NEIPv4Route]()
|
var ipv4IncludedRoutes = [NEIPv4Route]()
|
||||||
|
|
||||||
guard let splitTunnelSites else {
|
guard let splitTunnelSites else {
|
||||||
@@ -135,9 +393,8 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
|
effectiveSettings.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
|
||||||
} else {
|
} else if splitType == 2 {
|
||||||
if splitTunnelType == 2 {
|
|
||||||
var ipv4ExcludedRoutes = [NEIPv4Route]()
|
var ipv4ExcludedRoutes = [NEIPv4Route]()
|
||||||
var ipv4IncludedRoutes = [NEIPv4Route]()
|
var ipv4IncludedRoutes = [NEIPv4Route]()
|
||||||
var ipv6IncludedRoutes = [NEIPv6Route]()
|
var ipv6IncludedRoutes = [NEIPv6Route]()
|
||||||
@@ -165,14 +422,418 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
|||||||
destinationAddress: "\(allIPv6.address)",
|
destinationAddress: "\(allIPv6.address)",
|
||||||
networkPrefixLength: NSNumber(value: allIPv6.networkPrefixLength)))
|
networkPrefixLength: NSNumber(value: allIPv6.networkPrefixLength)))
|
||||||
}
|
}
|
||||||
networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
|
effectiveSettings.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
|
||||||
networkSettings?.ipv6Settings?.includedRoutes = ipv6IncludedRoutes
|
effectiveSettings.ipv6Settings?.includedRoutes = ipv6IncludedRoutes
|
||||||
networkSettings?.ipv4Settings?.excludedRoutes = ipv4ExcludedRoutes
|
effectiveSettings.ipv4Settings?.excludedRoutes = ipv4ExcludedRoutes
|
||||||
|
} else {
|
||||||
|
// Full tunnel: rely on adapter-provided routes.
|
||||||
|
}
|
||||||
|
|
||||||
|
if let serverEndpoint,
|
||||||
|
Self.isIPv4Address(serverEndpoint),
|
||||||
|
let ipv4Settings = effectiveSettings.ipv4Settings {
|
||||||
|
let hostMask = "255.255.255.255"
|
||||||
|
var excluded = ipv4Settings.excludedRoutes ?? []
|
||||||
|
let alreadyExcluded = excluded.contains {
|
||||||
|
$0.destinationAddress == serverEndpoint && $0.destinationSubnetMask == hostMask
|
||||||
|
}
|
||||||
|
if !alreadyExcluded {
|
||||||
|
excluded.append(NEIPv4Route(destinationAddress: serverEndpoint, subnetMask: hostMask))
|
||||||
|
ipv4Settings.excludedRoutes = excluded
|
||||||
|
ovpnLog(.info, title: "IPv4Routes", message: "excluded remoteAddress=\(serverEndpoint)")
|
||||||
|
}
|
||||||
|
} else if let serverEndpoint {
|
||||||
|
ovpnLog(.info, title: "IPv4Routes", message: "skip explicit remote exclude; non-ip server=\(serverEndpoint)")
|
||||||
|
}
|
||||||
|
|
||||||
|
let localAddr = openVpnLocalAddress
|
||||||
|
var net30Gateway: String?
|
||||||
|
if let localAddr, let mask = openVpnLocalMask {
|
||||||
|
net30Gateway = Self.net30Peer(for: localAddr, mask: mask)
|
||||||
|
}
|
||||||
|
var gateway = net30Gateway
|
||||||
|
if let adapterGateway = openVPNAdapter.connectionInformation?.gatewayIPv4, !adapterGateway.isEmpty {
|
||||||
|
if let localAddr, adapterGateway == localAddr {
|
||||||
|
ovpnLog(.info, title: "IPv4Gateway", message: "ignore adapter gateway equal to local address=\(adapterGateway)")
|
||||||
|
} else if let net30Gateway, net30Gateway != adapterGateway {
|
||||||
|
ovpnLog(.info, title: "IPv4Gateway", message: "ignore mismatched adapter gateway=\(adapterGateway), using net30 peer=\(net30Gateway)")
|
||||||
|
} else {
|
||||||
|
gateway = adapterGateway
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openVpnGatewayAddress = gateway
|
||||||
|
if let gateway, !gateway.isEmpty {
|
||||||
|
ovpnLog(.info, title: "IPv4Gateway", message: "gateway=\(gateway)")
|
||||||
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
if splitType == 0, let gateway, !gateway.isEmpty, effectiveSettings.tunnelRemoteAddress != gateway {
|
||||||
|
let updatedSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: gateway)
|
||||||
|
updatedSettings.ipv4Settings = effectiveSettings.ipv4Settings
|
||||||
|
updatedSettings.ipv6Settings = effectiveSettings.ipv6Settings
|
||||||
|
updatedSettings.dnsSettings = effectiveSettings.dnsSettings
|
||||||
|
updatedSettings.proxySettings = effectiveSettings.proxySettings
|
||||||
|
updatedSettings.mtu = effectiveSettings.mtu
|
||||||
|
effectiveSettings = updatedSettings
|
||||||
|
ovpnLog(.info, title: "Remote", message: "tunnelRemoteAddress set to gateway=\(gateway) on macOS full-tunnel")
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#if os(macOS)
|
||||||
|
if var ipv4Settings = effectiveSettings.ipv4Settings {
|
||||||
|
if splitType == 0 {
|
||||||
|
let hasNet30Mask = ipv4Settings.subnetMasks.contains("255.255.255.252")
|
||||||
|
if hasNet30Mask {
|
||||||
|
let normalizedMasks = Array(repeating: "255.255.255.255",
|
||||||
|
count: ipv4Settings.subnetMasks.count)
|
||||||
|
let normalized = NEIPv4Settings(addresses: ipv4Settings.addresses,
|
||||||
|
subnetMasks: normalizedMasks)
|
||||||
|
normalized.includedRoutes = ipv4Settings.includedRoutes
|
||||||
|
normalized.excludedRoutes = ipv4Settings.excludedRoutes
|
||||||
|
if #available(macOS 13.0, *) {
|
||||||
|
normalized.router = ipv4Settings.router
|
||||||
|
}
|
||||||
|
ipv4Settings = normalized
|
||||||
|
ovpnLog(.info, title: "IPv4Routes", message: "normalized net30 /30 masks to /32 on macOS full-tunnel")
|
||||||
|
}
|
||||||
|
|
||||||
|
if let gateway, !gateway.isEmpty {
|
||||||
|
if #available(macOS 13.0, *) {
|
||||||
|
ipv4Settings.router = gateway
|
||||||
|
ovpnLog(.info, title: "IPv4Routes", message: "set ipv4 router=\(gateway) on macOS full-tunnel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var included = ipv4Settings.includedRoutes ?? []
|
||||||
|
let hasDefault = included.contains {
|
||||||
|
$0.destinationAddress == "0.0.0.0" && $0.destinationSubnetMask == "0.0.0.0"
|
||||||
|
}
|
||||||
|
if hasDefault {
|
||||||
|
included.removeAll {
|
||||||
|
$0.destinationAddress == "0.0.0.0" && $0.destinationSubnetMask == "0.0.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let hasDef1Low = included.contains {
|
||||||
|
$0.destinationAddress == "0.0.0.0" && $0.destinationSubnetMask == "128.0.0.0"
|
||||||
|
}
|
||||||
|
let hasDef1High = included.contains {
|
||||||
|
$0.destinationAddress == "128.0.0.0" && $0.destinationSubnetMask == "128.0.0.0"
|
||||||
|
}
|
||||||
|
if (hasDefault || openVpnRedirectGatewayDef1) && !(hasDef1Low && hasDef1High) {
|
||||||
|
if !hasDef1Low {
|
||||||
|
let route = NEIPv4Route(destinationAddress: "0.0.0.0", subnetMask: "128.0.0.0")
|
||||||
|
if let gateway, !gateway.isEmpty {
|
||||||
|
route.gatewayAddress = gateway
|
||||||
|
}
|
||||||
|
included.append(route)
|
||||||
|
}
|
||||||
|
if !hasDef1High {
|
||||||
|
let route = NEIPv4Route(destinationAddress: "128.0.0.0", subnetMask: "128.0.0.0")
|
||||||
|
if let gateway, !gateway.isEmpty {
|
||||||
|
route.gatewayAddress = gateway
|
||||||
|
}
|
||||||
|
included.append(route)
|
||||||
|
}
|
||||||
|
ovpnLog(.info, title: "IPv4Routes", message: "ensured def1 routes (/1 + /1) on macOS full-tunnel")
|
||||||
|
}
|
||||||
|
if let gateway, !gateway.isEmpty {
|
||||||
|
included = included.map { route in
|
||||||
|
let isDef1 =
|
||||||
|
(route.destinationAddress == "0.0.0.0" && route.destinationSubnetMask == "128.0.0.0") ||
|
||||||
|
(route.destinationAddress == "128.0.0.0" && route.destinationSubnetMask == "128.0.0.0")
|
||||||
|
guard isDef1 else { return route }
|
||||||
|
if route.gatewayAddress == gateway {
|
||||||
|
return route
|
||||||
|
}
|
||||||
|
let updatedRoute = NEIPv4Route(destinationAddress: route.destinationAddress,
|
||||||
|
subnetMask: route.destinationSubnetMask)
|
||||||
|
updatedRoute.gatewayAddress = gateway
|
||||||
|
return updatedRoute
|
||||||
|
}
|
||||||
|
ovpnLog(.info, title: "IPv4Routes", message: "set gateway=\(gateway) on macOS def1 routes")
|
||||||
|
}
|
||||||
|
ipv4Settings.includedRoutes = included
|
||||||
|
effectiveSettings.ipv4Settings = ipv4Settings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if let ipv4Settings = effectiveSettings.ipv4Settings {
|
||||||
|
let included = (ipv4Settings.includedRoutes ?? []).map {
|
||||||
|
let gw = $0.gatewayAddress ?? ""
|
||||||
|
return "\($0.destinationAddress)/\($0.destinationSubnetMask) gw=\(gw)"
|
||||||
|
}
|
||||||
|
let excluded = (ipv4Settings.excludedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationSubnetMask)" }
|
||||||
|
let addresses = ipv4Settings.addresses.joined(separator: ",")
|
||||||
|
let masks = ipv4Settings.subnetMasks.joined(separator: ",")
|
||||||
|
let router: String
|
||||||
|
#if os(macOS)
|
||||||
|
if #available(macOS 13.0, *) {
|
||||||
|
router = ipv4Settings.router ?? ""
|
||||||
|
} else {
|
||||||
|
router = ""
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
router = ""
|
||||||
|
#endif
|
||||||
|
ovpnLog(.info, title: "IPv4Routes", message: "addresses=[\(addresses)] masks=[\(masks)] router=\(router) included=\(included) excluded=\(excluded)")
|
||||||
|
} else {
|
||||||
|
ovpnLog(.error, title: "IPv4Routes", message: "ipv4Settings is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if let ipv6Settings = effectiveSettings.ipv6Settings {
|
||||||
|
let included = (ipv6Settings.includedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationNetworkPrefixLength)" }
|
||||||
|
let excluded = (ipv6Settings.excludedRoutes ?? []).map { "\($0.destinationAddress)/\($0.destinationNetworkPrefixLength)" }
|
||||||
|
let addresses = ipv6Settings.addresses.joined(separator: ",")
|
||||||
|
let prefixes = ipv6Settings.networkPrefixLengths.map { "\($0)" }.joined(separator: ",")
|
||||||
|
ovpnLog(.info, title: "IPv6Routes", message: "addresses=[\(addresses)] prefixes=[\(prefixes)] included=\(included) excluded=\(excluded)")
|
||||||
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
if effectiveSettings.ipv6Settings != nil {
|
||||||
|
effectiveSettings.ipv6Settings = nil
|
||||||
|
ovpnLog(.info, title: "IPv6", message: "cleared ipv6Settings on macOS")
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
lastOpenVPNSettings = effectiveSettings
|
||||||
|
|
||||||
// Set the network settings for the current tunneling session.
|
// Set the network settings for the current tunneling session.
|
||||||
setTunnelNetworkSettings(networkSettings, completionHandler: completionHandler)
|
setTunnelNetworkSettings(effectiveSettings) { error in
|
||||||
|
if let error {
|
||||||
|
ovpnLog(.error, title: "SetTunnelNetworkSettings", message: error.localizedDescription)
|
||||||
|
} else {
|
||||||
|
ovpnLog(.info, title: "SetTunnelNetworkSettings", message: "ok")
|
||||||
|
}
|
||||||
|
completionHandler(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func extractDnsServers(from config: String) -> [String] {
|
||||||
|
let lines = config.split(whereSeparator: \.isNewline)
|
||||||
|
var servers: [String] = []
|
||||||
|
for line in lines {
|
||||||
|
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if trimmed.hasPrefix("dhcp-option DNS ") {
|
||||||
|
let parts = trimmed.split(separator: " ")
|
||||||
|
if let last = parts.last {
|
||||||
|
servers.append(String(last))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return servers
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func extractRemoteHost(from config: String) -> String? {
|
||||||
|
let lines = config.split(whereSeparator: \.isNewline)
|
||||||
|
for line in lines {
|
||||||
|
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if trimmed.hasPrefix("remote ") {
|
||||||
|
let parts = trimmed.split(separator: " ")
|
||||||
|
if parts.count >= 2 {
|
||||||
|
return String(parts[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func hasRedirectGatewayDef1(in config: String) -> Bool {
|
||||||
|
let lines = config.split(whereSeparator: \.isNewline)
|
||||||
|
for line in lines {
|
||||||
|
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if trimmed.hasPrefix("redirect-gateway") {
|
||||||
|
return trimmed.split(whereSeparator: { $0 == " " || $0 == "\t" }).contains("def1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func net30Peer(for address: String, mask: String) -> String? {
|
||||||
|
guard mask == "255.255.255.252" else { return nil }
|
||||||
|
let parts = address.split(separator: ".")
|
||||||
|
guard parts.count == 4 else { return nil }
|
||||||
|
var octets: [Int] = []
|
||||||
|
for part in parts {
|
||||||
|
guard let num = Int(part), num >= 0 && num <= 255 else { return nil }
|
||||||
|
octets.append(num)
|
||||||
|
}
|
||||||
|
let ip = (octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3]
|
||||||
|
let network = ip & ~3
|
||||||
|
let host = ip - network
|
||||||
|
let peerHost: Int
|
||||||
|
switch host {
|
||||||
|
case 1: peerHost = 2
|
||||||
|
case 2: peerHost = 1
|
||||||
|
default: return nil
|
||||||
|
}
|
||||||
|
let peerIP = network + peerHost
|
||||||
|
return "\((peerIP >> 24) & 0xff).\((peerIP >> 16) & 0xff).\((peerIP >> 8) & 0xff).\(peerIP & 0xff)"
|
||||||
|
}
|
||||||
|
|
||||||
|
private func logOpenVPNConnectionInfo() {
|
||||||
|
guard let info = ovpnAdapter?.connectionInformation else { return }
|
||||||
|
let message = "vpnIPv4=\(info.vpnIPv4 ?? "") gatewayIPv4=\(info.gatewayIPv4 ?? "") serverIP=\(info.serverIP ?? "") tun=\(info.tunName ?? "")"
|
||||||
|
ovpnLog(.info, title: "ConnInfo", message: message)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func normalizeInlineBlock(
|
||||||
|
in config: String,
|
||||||
|
tag: String,
|
||||||
|
beginMarkers: [String],
|
||||||
|
endMarkers: [String]
|
||||||
|
) -> String {
|
||||||
|
guard !beginMarkers.isEmpty, !endMarkers.isEmpty else { return config }
|
||||||
|
|
||||||
|
var normalizedConfig = config
|
||||||
|
let openTag = "<\(tag)>"
|
||||||
|
let closeTag = "</\(tag)>"
|
||||||
|
var searchStart = normalizedConfig.startIndex
|
||||||
|
|
||||||
|
while let openRange = normalizedConfig.range(of: openTag, range: searchStart..<normalizedConfig.endIndex),
|
||||||
|
let closeRange = normalizedConfig.range(of: closeTag, range: openRange.upperBound..<normalizedConfig.endIndex) {
|
||||||
|
let rawBody = String(normalizedConfig[openRange.upperBound..<closeRange.lowerBound])
|
||||||
|
let lines = rawBody
|
||||||
|
.split(whereSeparator: \.isNewline)
|
||||||
|
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||||
|
.filter { !$0.isEmpty }
|
||||||
|
|
||||||
|
var beginIndex: Int?
|
||||||
|
var endIndex: Int?
|
||||||
|
for (idx, line) in lines.enumerated() {
|
||||||
|
if beginIndex == nil,
|
||||||
|
beginMarkers.contains(where: { line.contains($0) }) {
|
||||||
|
beginIndex = idx
|
||||||
|
}
|
||||||
|
if beginIndex != nil,
|
||||||
|
endMarkers.contains(where: { line.contains($0) }) {
|
||||||
|
endIndex = idx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let beginIndex,
|
||||||
|
let endIndex,
|
||||||
|
endIndex >= beginIndex {
|
||||||
|
let extracted = lines[beginIndex...endIndex].joined(separator: "\n")
|
||||||
|
let replacement = "<\(tag)>\n\(extracted)\n</\(tag)>"
|
||||||
|
normalizedConfig.replaceSubrange(openRange.lowerBound..<closeRange.upperBound, with: replacement)
|
||||||
|
ovpnLog(.info, title: "ConfigInline", message: "tag=<\(tag)> linesIn=\(lines.count) linesOut=\(endIndex - beginIndex + 1)")
|
||||||
|
searchStart = normalizedConfig.index(openRange.lowerBound, offsetBy: replacement.count)
|
||||||
|
} else {
|
||||||
|
ovpnLog(.error, title: "ConfigInline", message: "tag=<\(tag)> missing markers, keeping original body")
|
||||||
|
searchStart = closeRange.upperBound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static func stripUnsupportedOptions(forOpenVPNAdapter config: String) -> String {
|
||||||
|
let unsupportedTokens: Set<String> = [
|
||||||
|
"block-ipv6",
|
||||||
|
"script-security",
|
||||||
|
"up",
|
||||||
|
"down",
|
||||||
|
"resolv-retry",
|
||||||
|
"persist-key",
|
||||||
|
"persist-tun",
|
||||||
|
"compat-mode",
|
||||||
|
"disable-dco"
|
||||||
|
]
|
||||||
|
let inlineBlockTags: Set<String> = [
|
||||||
|
"ca",
|
||||||
|
"cert",
|
||||||
|
"key",
|
||||||
|
"pkcs12",
|
||||||
|
"tls-auth",
|
||||||
|
"tls-crypt",
|
||||||
|
"tls-crypt-v2",
|
||||||
|
"secret",
|
||||||
|
"crl-verify",
|
||||||
|
"extra-certs"
|
||||||
|
]
|
||||||
|
|
||||||
|
var removed: [String: Int] = [:]
|
||||||
|
var normalized: [String: Int] = [:]
|
||||||
|
var output: [String] = []
|
||||||
|
var activeInlineTag: String?
|
||||||
|
|
||||||
|
for rawLine in config.split(whereSeparator: \.isNewline) {
|
||||||
|
let line = String(rawLine)
|
||||||
|
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if trimmed.isEmpty {
|
||||||
|
output.append(line)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let trimmedLowercased = trimmed.lowercased()
|
||||||
|
|
||||||
|
if let currentInlineTag = activeInlineTag {
|
||||||
|
output.append(line)
|
||||||
|
if trimmedLowercased == "</\(currentInlineTag)>" {
|
||||||
|
activeInlineTag = nil
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if trimmedLowercased.hasPrefix("<"),
|
||||||
|
trimmedLowercased.hasSuffix(">"),
|
||||||
|
!trimmedLowercased.hasPrefix("</") {
|
||||||
|
let tagContent = String(trimmedLowercased.dropFirst().dropLast())
|
||||||
|
let tagName = tagContent
|
||||||
|
.split(whereSeparator: { $0 == " " || $0 == "\t" })
|
||||||
|
.first
|
||||||
|
.map(String.init) ?? ""
|
||||||
|
if inlineBlockTags.contains(tagName) {
|
||||||
|
activeInlineTag = tagName
|
||||||
|
output.append(line)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if trimmed.hasPrefix("#") || trimmed.hasPrefix(";") {
|
||||||
|
output.append(line)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let parts = trimmed.split(whereSeparator: { $0 == " " || $0 == "\t" })
|
||||||
|
let token = parts.first.map(String.init)?.lowercased() ?? ""
|
||||||
|
if trimmedLowercased.hasPrefix("redirect-gateway") || token.hasPrefix("redirect-gateway") {
|
||||||
|
let hasDef1 = parts.dropFirst().contains { String($0).lowercased().hasPrefix("def1") }
|
||||||
|
if hasDef1 {
|
||||||
|
output.append("redirect-gateway def1")
|
||||||
|
normalized["redirect-gateway", default: 0] += 1
|
||||||
|
} else {
|
||||||
|
removed["redirect-gateway", default: 0] += 1
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if let matchedUnsupported = unsupportedTokens.first(where: { token.hasPrefix($0) }) {
|
||||||
|
removed[matchedUnsupported, default: 0] += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
output.append(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !removed.isEmpty {
|
||||||
|
let summary = removed.keys.sorted().map { "\($0)=\(removed[$0] ?? 0)" }.joined(separator: " ")
|
||||||
|
ovpnLog(.info, title: "ConfigStrip", message: summary)
|
||||||
|
}
|
||||||
|
if !normalized.isEmpty {
|
||||||
|
let summary = normalized.keys.sorted().map { "\($0)=\(normalized[$0] ?? 0)" }.joined(separator: " ")
|
||||||
|
ovpnLog(.info, title: "ConfigNormalize", message: summary)
|
||||||
|
}
|
||||||
|
|
||||||
|
return output.joined(separator: "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func isIPv4Address(_ value: String) -> Bool {
|
||||||
|
let parts = value.split(separator: ".")
|
||||||
|
if parts.count != 4 { return false }
|
||||||
|
for part in parts {
|
||||||
|
guard let num = Int(part), num >= 0 && num <= 255 else { return false }
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process events returned by the OpenVPN library
|
// Process events returned by the OpenVPN library
|
||||||
@@ -190,6 +851,9 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
|||||||
|
|
||||||
startHandler(nil)
|
startHandler(nil)
|
||||||
self.startHandler = nil
|
self.startHandler = nil
|
||||||
|
|
||||||
|
logOpenVPNConnectionInfo()
|
||||||
|
refreshOpenVPNSettingsAfterConnect()
|
||||||
case .disconnected:
|
case .disconnected:
|
||||||
guard let stopHandler = stopHandler else { return }
|
guard let stopHandler = stopHandler else { return }
|
||||||
|
|
||||||
@@ -208,8 +872,11 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
|||||||
|
|
||||||
// Handle errors thrown by the OpenVPN library
|
// Handle errors thrown by the OpenVPN library
|
||||||
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleError error: Error) {
|
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleError error: Error) {
|
||||||
|
let nsError = error as NSError
|
||||||
|
logOpenVPNError(nsError)
|
||||||
|
|
||||||
// Handle only fatal errors
|
// 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 }
|
fatal == true else { return }
|
||||||
|
|
||||||
if vpnReachability.isTracking {
|
if vpnReachability.isTracking {
|
||||||
@@ -229,6 +896,41 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
|||||||
// Handle log messages
|
// Handle log messages
|
||||||
ovpnLog(.info, message: logMessage)
|
ovpnLog(.info, message: logMessage)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension NEPacketTunnelFlow: OpenVPNAdapterPacketFlow {}
|
func openVPNAdapterDidReceiveClockTick(_ openVPNAdapter: OpenVPNAdapter) {
|
||||||
|
let now = Date()
|
||||||
|
if now.timeIntervalSince(lastOpenVPNStatsLogTime) < 5 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lastOpenVPNStatsLogTime = now
|
||||||
|
|
||||||
|
let transport = openVPNAdapter.transportStatistics
|
||||||
|
let iface = openVPNAdapter.interfaceStatistics
|
||||||
|
let transportLine = "transport bytesIn=\(transport.bytesIn) bytesOut=\(transport.bytesOut) packetsIn=\(transport.packetsIn) packetsOut=\(transport.packetsOut)"
|
||||||
|
let ifaceLine = "iface bytesIn=\(iface.bytesIn) bytesOut=\(iface.bytesOut) packetsIn=\(iface.packetsIn) packetsOut=\(iface.packetsOut) errorsIn=\(iface.errorsIn) errorsOut=\(iface.errorsOut)"
|
||||||
|
ovpnLog(.info, title: "Stats", message: "\(transportLine) | \(ifaceLine)")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func refreshOpenVPNSettingsAfterConnect() {
|
||||||
|
let localAddr = openVpnLocalAddress
|
||||||
|
var net30Gateway: String?
|
||||||
|
if let localAddr, let mask = openVpnLocalMask {
|
||||||
|
net30Gateway = Self.net30Peer(for: localAddr, mask: mask)
|
||||||
|
}
|
||||||
|
var gateway = net30Gateway
|
||||||
|
if let adapterGateway = ovpnAdapter?.connectionInformation?.gatewayIPv4, !adapterGateway.isEmpty {
|
||||||
|
if let localAddr, adapterGateway == localAddr {
|
||||||
|
ovpnLog(.info, title: "IPv4Gateway", message: "post-connect ignoring adapter gateway equal to local address=\(adapterGateway)")
|
||||||
|
} else if let net30Gateway, net30Gateway != adapterGateway {
|
||||||
|
ovpnLog(.info, title: "IPv4Gateway", message: "post-connect keeping net30 peer=\(net30Gateway), adapter gateway=\(adapterGateway)")
|
||||||
|
} else {
|
||||||
|
gateway = adapterGateway
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let gateway, !gateway.isEmpty else { return }
|
||||||
|
openVpnGatewayAddress = gateway
|
||||||
|
ovpnLog(.info, title: "IPv4Gateway", message: "post-connect gateway=\(gateway)")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -94,15 +94,24 @@ extension PacketTunnelProvider {
|
|||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
wg_log(.error, message: "Can't parse WG config: \(error.localizedDescription)")
|
wg_log(.error, message: "Can't parse WG config: \(error.localizedDescription)")
|
||||||
completionHandler(nil)
|
errorNotifier.notify(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid)
|
||||||
|
completionHandler(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleWireguardStatusMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
|
func handleWireguardStatusMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
|
||||||
guard let completionHandler = completionHandler else { return }
|
guard let completionHandler = completionHandler else { return }
|
||||||
wgAdapter?.getRuntimeConfiguration { settings in
|
guard let wgAdapter = wgAdapter else {
|
||||||
let components = settings!.components(separatedBy: "\n")
|
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] = [:]
|
var settingsDictionary: [String: String] = [:]
|
||||||
for component in components {
|
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 }
|
guard let completionHandler = completionHandler else { return }
|
||||||
if messageData.count == 1 && messageData[0] == 0 {
|
if messageData.count == 1 && messageData[0] == 0 {
|
||||||
wgAdapter?.getRuntimeConfiguration { settings in
|
wgAdapter?.getRuntimeConfiguration { settings in
|
||||||
@@ -176,7 +185,7 @@ extension PacketTunnelProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func stopWireguard(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
|
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
|
wgAdapter?.stop { error in
|
||||||
ErrorNotifier.removeLastErrorFile()
|
ErrorNotifier.removeLastErrorFile()
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
|
import Darwin
|
||||||
import Foundation
|
import Foundation
|
||||||
import NetworkExtension
|
import NetworkExtension
|
||||||
import WireGuardKitGo
|
|
||||||
|
|
||||||
enum XrayErrors: Error {
|
enum XrayErrors: Error {
|
||||||
case noXrayConfig
|
case noXrayConfig
|
||||||
case xrayConfigIsWrong
|
case xrayConfigIsWrong
|
||||||
case cantSaveXrayConfig
|
case cantSaveXrayConfig
|
||||||
case cantParseListenAndPort
|
case cantParseListenAndPort
|
||||||
|
case cantAcquireLocalPort
|
||||||
case cantSaveHevSocksConfig
|
case cantSaveHevSocksConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,6 +23,80 @@ extension Constants {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension PacketTunnelProvider {
|
extension PacketTunnelProvider {
|
||||||
|
/// TCP port chosen by the OS on IPv6 loopback (::1), matching inbound listen address.
|
||||||
|
private func acquireFreeLocalPort() throws -> Int {
|
||||||
|
let fd = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP)
|
||||||
|
guard fd != -1 else {
|
||||||
|
throw XrayErrors.cantAcquireLocalPort
|
||||||
|
}
|
||||||
|
defer { close(fd) }
|
||||||
|
var reuse: Int32 = 1
|
||||||
|
_ = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, socklen_t(MemoryLayout<Int32>.size))
|
||||||
|
var addr = sockaddr_in6()
|
||||||
|
addr.sin6_len = UInt8(MemoryLayout<sockaddr_in6>.size)
|
||||||
|
addr.sin6_family = sa_family_t(AF_INET6)
|
||||||
|
addr.sin6_port = in_port_t(0).bigEndian
|
||||||
|
addr.sin6_addr = in6addr_loopback
|
||||||
|
addr.sin6_scope_id = 0
|
||||||
|
let bindResult = withUnsafePointer(to: &addr) { ptr in
|
||||||
|
ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { p in
|
||||||
|
bind(fd, p, socklen_t(MemoryLayout<sockaddr_in6>.size))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
guard bindResult == 0 else {
|
||||||
|
throw XrayErrors.cantAcquireLocalPort
|
||||||
|
}
|
||||||
|
var bound = sockaddr_in6()
|
||||||
|
var len = socklen_t(MemoryLayout<sockaddr_in6>.size)
|
||||||
|
let gr = withUnsafeMutablePointer(to: &bound) { p in
|
||||||
|
p.withMemoryRebound(to: sockaddr.self, capacity: 1) { bp in
|
||||||
|
getsockname(fd, bp, &len)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
guard gr == 0 else {
|
||||||
|
throw XrayErrors.cantAcquireLocalPort
|
||||||
|
}
|
||||||
|
return Int(bound.sin6_port.byteSwapped)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyXraySplitTunnel(_ xrayConfig: XrayConfig,
|
||||||
|
settings: NEPacketTunnelNetworkSettings) {
|
||||||
|
guard let splitTunnelType = xrayConfig.splitTunnelType else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let splitTunnelSites = xrayConfig.splitTunnelSites else {
|
||||||
|
xrayLog(.error, message: "Split tunnel sites are not set")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if splitTunnelType == 1 {
|
||||||
|
var ipv4IncludedRoutes = [NEIPv4Route]()
|
||||||
|
|
||||||
|
for allowedIPString in splitTunnelSites {
|
||||||
|
if let allowedIP = IPAddressRange(from: allowedIPString) {
|
||||||
|
ipv4IncludedRoutes.append(NEIPv4Route(
|
||||||
|
destinationAddress: "\(allowedIP.address)",
|
||||||
|
subnetMask: "\(allowedIP.subnetMask())"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
|
||||||
|
} else if splitTunnelType == 2 {
|
||||||
|
var ipv4ExcludedRoutes = [NEIPv4Route]()
|
||||||
|
|
||||||
|
for excludedIPString in splitTunnelSites {
|
||||||
|
if let excludedIP = IPAddressRange(from: excludedIPString) {
|
||||||
|
ipv4ExcludedRoutes.append(NEIPv4Route(
|
||||||
|
destinationAddress: "\(excludedIP.address)",
|
||||||
|
subnetMask: "\(excludedIP.subnetMask())"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ipv4Settings?.excludedRoutes = ipv4ExcludedRoutes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func startXray(completionHandler: @escaping (Error?) -> Void) {
|
func startXray(completionHandler: @escaping (Error?) -> Void) {
|
||||||
|
|
||||||
// Xray configuration
|
// Xray configuration
|
||||||
@@ -73,6 +148,7 @@ extension PacketTunnelProvider {
|
|||||||
settings.dnsSettings = !dnsArray.isEmpty
|
settings.dnsSettings = !dnsArray.isEmpty
|
||||||
? NEDNSSettings(servers: dnsArray)
|
? NEDNSSettings(servers: dnsArray)
|
||||||
: NEDNSSettings(servers: ["1.1.1.1"])
|
: NEDNSSettings(servers: ["1.1.1.1"])
|
||||||
|
applyXraySplitTunnel(xrayConfig, settings: settings)
|
||||||
|
|
||||||
let xrayConfigData = xrayConfig.config.data(using: .utf8)
|
let xrayConfigData = xrayConfig.config.data(using: .utf8)
|
||||||
|
|
||||||
@@ -91,14 +167,11 @@ extension PacketTunnelProvider {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let port = 10808
|
let port = try acquireFreeLocalPort()
|
||||||
let address = "::1"
|
let address = "::1"
|
||||||
|
|
||||||
if var inboundsArray = jsonDict["inbounds"] as? [[String: Any]], !inboundsArray.isEmpty {
|
// Extract existing SOCKS5 credentials or generate new ones per session.
|
||||||
inboundsArray[0]["port"] = port
|
let socksCredentials = ensureInboundAuth(jsonDict: &jsonDict, port: port, address: address)
|
||||||
inboundsArray[0]["listen"] = address
|
|
||||||
jsonDict["inbounds"] = inboundsArray
|
|
||||||
}
|
|
||||||
|
|
||||||
let updatedData = try JSONSerialization.data(withJSONObject: jsonDict, options: [])
|
let updatedData = try JSONSerialization.data(withJSONObject: jsonDict, options: [])
|
||||||
|
|
||||||
@@ -108,6 +181,8 @@ extension PacketTunnelProvider {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self?.updateActiveInterfaceIndexForCurrentPath()
|
||||||
|
|
||||||
// Launch xray
|
// Launch xray
|
||||||
self?.setupAndStartXray(configData: updatedData) { xrayError in
|
self?.setupAndStartXray(configData: updatedData) { xrayError in
|
||||||
if let xrayError {
|
if let xrayError {
|
||||||
@@ -119,6 +194,8 @@ extension PacketTunnelProvider {
|
|||||||
self?.setupAndRunTun2socks(configData: updatedData,
|
self?.setupAndRunTun2socks(configData: updatedData,
|
||||||
address: address,
|
address: address,
|
||||||
port: port,
|
port: port,
|
||||||
|
username: socksCredentials.username,
|
||||||
|
password: socksCredentials.password,
|
||||||
completionHandler: completionHandler)
|
completionHandler: completionHandler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,6 +211,71 @@ extension PacketTunnelProvider {
|
|||||||
completionHandler()
|
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 struct SocksCredentials {
|
||||||
|
let username: String
|
||||||
|
let password: String
|
||||||
|
}
|
||||||
|
|
||||||
|
private func indexOfSocksInbound(in inboundsArray: [[String: Any]]) -> Int? {
|
||||||
|
for (i, inbound) in inboundsArray.enumerated() {
|
||||||
|
guard let proto = inbound["protocol"] as? String else { continue }
|
||||||
|
if proto.caseInsensitiveCompare("socks") == .orderedSame {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns existing SOCKS5 credentials from the inbound config, or generates and injects
|
||||||
|
// new random ones. Also sets port and address on the socks inbound entry.
|
||||||
|
private func ensureInboundAuth(jsonDict: inout [String: Any], port: Int, address: String) -> SocksCredentials {
|
||||||
|
var inboundsArray = jsonDict["inbounds"] as? [[String: Any]] ?? []
|
||||||
|
|
||||||
|
if let socksIdx = indexOfSocksInbound(in: inboundsArray) {
|
||||||
|
var inbound = inboundsArray[socksIdx]
|
||||||
|
inbound["port"] = port
|
||||||
|
inbound["listen"] = address
|
||||||
|
|
||||||
|
var settings = inbound["settings"] as? [String: Any] ?? [:]
|
||||||
|
if let accounts = settings["accounts"] as? [[String: Any]],
|
||||||
|
let first = accounts.first,
|
||||||
|
let user = first["user"] as? String, !user.isEmpty,
|
||||||
|
let pass = first["pass"] as? String, !pass.isEmpty {
|
||||||
|
// Re-use existing credentials, but always enforce auth mode in case the
|
||||||
|
// imported config had accounts but auth: "noauth" (or no auth field).
|
||||||
|
settings["auth"] = "password"
|
||||||
|
inbound["settings"] = settings
|
||||||
|
inboundsArray[socksIdx] = inbound
|
||||||
|
jsonDict["inbounds"] = inboundsArray
|
||||||
|
return SocksCredentials(username: user, password: pass)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new random credentials for this session
|
||||||
|
let user = UUID().uuidString.replacingOccurrences(of: "-", with: "").lowercased().prefix(16)
|
||||||
|
let pass = UUID().uuidString.replacingOccurrences(of: "-", with: "").lowercased()
|
||||||
|
settings["auth"] = "password"
|
||||||
|
settings["accounts"] = [["user": String(user), "pass": pass]]
|
||||||
|
inbound["settings"] = settings
|
||||||
|
inboundsArray[socksIdx] = inbound
|
||||||
|
jsonDict["inbounds"] = inboundsArray
|
||||||
|
return SocksCredentials(username: String(user), password: pass)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: no socks inbound — generate credentials but can't inject
|
||||||
|
let user = UUID().uuidString.replacingOccurrences(of: "-", with: "").lowercased().prefix(16)
|
||||||
|
let pass = UUID().uuidString.replacingOccurrences(of: "-", with: "").lowercased()
|
||||||
|
return SocksCredentials(username: String(user), password: pass)
|
||||||
|
}
|
||||||
|
|
||||||
private func setupAndStartXray(configData: Data,
|
private func setupAndStartXray(configData: Data,
|
||||||
completionHandler: @escaping (Error?) -> Void) {
|
completionHandler: @escaping (Error?) -> Void) {
|
||||||
let path = Constants.cachesDirectory.appendingPathComponent("config.json", isDirectory: false).path
|
let path = Constants.cachesDirectory.appendingPathComponent("config.json", isDirectory: false).path
|
||||||
@@ -143,6 +285,17 @@ extension PacketTunnelProvider {
|
|||||||
return
|
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,
|
LibXrayRunXray(nil,
|
||||||
path,
|
path,
|
||||||
Int64.max)
|
Int64.max)
|
||||||
@@ -154,6 +307,8 @@ extension PacketTunnelProvider {
|
|||||||
private func setupAndRunTun2socks(configData: Data,
|
private func setupAndRunTun2socks(configData: Data,
|
||||||
address: String,
|
address: String,
|
||||||
port: Int,
|
port: Int,
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
completionHandler: @escaping (Error?) -> Void) {
|
completionHandler: @escaping (Error?) -> Void) {
|
||||||
let config = """
|
let config = """
|
||||||
tunnel:
|
tunnel:
|
||||||
@@ -161,6 +316,8 @@ extension PacketTunnelProvider {
|
|||||||
socks5:
|
socks5:
|
||||||
port: \(port)
|
port: \(port)
|
||||||
address: \(address)
|
address: \(address)
|
||||||
|
username: \(username)
|
||||||
|
password: \(password)
|
||||||
udp: 'udp'
|
udp: 'udp'
|
||||||
misc:
|
misc:
|
||||||
task-stack-size: 20480
|
task-stack-size: 20480
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import NetworkExtension
|
import NetworkExtension
|
||||||
|
import Network
|
||||||
import os
|
import os
|
||||||
import Darwin
|
import Darwin
|
||||||
import OpenVPNAdapter
|
import OpenVPNAdapter
|
||||||
@@ -38,17 +39,126 @@ struct Constants {
|
|||||||
class PacketTunnelProvider: NEPacketTunnelProvider {
|
class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||||
var wgAdapter: WireGuardAdapter?
|
var wgAdapter: WireGuardAdapter?
|
||||||
var ovpnAdapter: OpenVPNAdapter?
|
var ovpnAdapter: OpenVPNAdapter?
|
||||||
|
private lazy var openVPNPacketFlowAdapter = PacketTunnelFlowAdapter(flow: packetFlow)
|
||||||
|
private let pathMonitorQueue = DispatchQueue(label: Constants.processQueueName + ".path-monitor")
|
||||||
|
private let networkChangeQueue = DispatchQueue(label: Constants.processQueueName + ".network-change")
|
||||||
|
private let pathMonitor = NWPathMonitor()
|
||||||
|
private var didReceiveInitialPathUpdate = false
|
||||||
|
private var currentPath: Network.NWPath?
|
||||||
|
private var currentPathSignature: String?
|
||||||
|
private var pendingOpenVPNReconnectWorkItem: DispatchWorkItem?
|
||||||
|
private var pendingNetworkChangeWorkItem: DispatchWorkItem?
|
||||||
|
private var isApplyingNetworkChange = false
|
||||||
|
private var lastOpenVPNReachabilityStatus: OpenVPNReachabilityStatus?
|
||||||
|
|
||||||
var splitTunnelType: Int?
|
var splitTunnelType: Int?
|
||||||
var splitTunnelSites: [String]?
|
var splitTunnelSites: [String]?
|
||||||
|
var openVpnDnsServers: [String] = []
|
||||||
|
var openVpnRemoteAddress: String?
|
||||||
|
var openVpnRedirectGatewayDef1 = false
|
||||||
|
var openVpnLocalAddress: String?
|
||||||
|
var openVpnLocalMask: String?
|
||||||
|
var openVpnGatewayAddress: String?
|
||||||
|
var lastOpenVPNSettings: NEPacketTunnelNetworkSettings?
|
||||||
|
var lastOpenVPNStatsLogTime = Date.distantPast
|
||||||
|
|
||||||
let vpnReachability = OpenVPNReachability()
|
let vpnReachability = OpenVPNReachability()
|
||||||
|
|
||||||
var startHandler: ((Error?) -> Void)?
|
var startHandler: ((Error?) -> Void)?
|
||||||
var stopHandler: (() -> Void)?
|
var stopHandler: (() -> Void)?
|
||||||
var protoType: TunnelProtoType?
|
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 and OpenVPN manages network changes internally in its own adapter.
|
||||||
|
if proto == .wireguard || proto == .openvpn {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if proto == .openvpn {
|
||||||
|
self.scheduleOpenVPNReconnect(reason: "NWPath changed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.isApplyingNetworkChange || self.reasserting {
|
||||||
|
xrayLog(.debug, message: "Ignoring path change while xray restart is in progress")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.scheduleNetworkChangeHandling(for: proto, path: path)
|
||||||
|
}
|
||||||
|
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) {
|
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 {
|
guard let message = String(data: messageData, encoding: .utf8) else {
|
||||||
if let completionHandler {
|
if let completionHandler {
|
||||||
completionHandler(nil)
|
completionHandler(nil)
|
||||||
@@ -59,6 +169,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||||||
neLog(.info, title: "App said: ", message: message)
|
neLog(.info, title: "App said: ", message: message)
|
||||||
|
|
||||||
guard let message = try? JSONSerialization.jsonObject(with: messageData, options: []) as? [String: Any] else {
|
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")
|
neLog(.error, message: "Failed to serialize message from app")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -86,9 +200,26 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||||||
let errorNotifier = ErrorNotifier(activationAttemptId: activationAttemptId)
|
let errorNotifier = ErrorNotifier(activationAttemptId: activationAttemptId)
|
||||||
|
|
||||||
neLog(.info, message: "Start tunnel")
|
neLog(.info, message: "Start tunnel")
|
||||||
|
if let vpnProto = protocolConfiguration as? NEVPNProtocol {
|
||||||
|
if #available(iOS 14.0, macOS 11.0, *) {
|
||||||
|
var details = "includeAllNetworks=\(vpnProto.includeAllNetworks)"
|
||||||
|
if #available(iOS 14.2, macOS 11.0, *) {
|
||||||
|
details += " excludeLocalNetworks=\(vpnProto.excludeLocalNetworks)"
|
||||||
|
}
|
||||||
|
neLog(.info, title: "Protocol", message: details)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let protocolConfiguration = protocolConfiguration as? NETunnelProviderProtocol {
|
if let protocolConfiguration = protocolConfiguration as? NETunnelProviderProtocol {
|
||||||
let providerConfiguration = protocolConfiguration.providerConfiguration
|
let providerConfiguration = protocolConfiguration.providerConfiguration
|
||||||
|
let providerKeys = providerConfiguration?.keys.sorted().joined(separator: ",") ?? ""
|
||||||
|
var protocolDetails = "bundleId=\(protocolConfiguration.providerBundleIdentifier ?? "") keys=[\(providerKeys)]"
|
||||||
|
if let ovpnData = providerConfiguration?[Constants.ovpnConfigKey] as? Data {
|
||||||
|
let preview = String(decoding: ovpnData.prefix(512), as: UTF8.self)
|
||||||
|
protocolDetails += " ovpnBytes=\(ovpnData.count) ovpnPreview=\(preview)"
|
||||||
|
}
|
||||||
|
neLog(.info, title: "Protocol", message: protocolDetails)
|
||||||
|
|
||||||
if (providerConfiguration?[Constants.ovpnConfigKey] as? Data) != nil {
|
if (providerConfiguration?[Constants.ovpnConfigKey] as? Data) != nil {
|
||||||
protoType = .openvpn
|
protoType = .openvpn
|
||||||
} else if (providerConfiguration?[Constants.wireGuardConfigKey] as? Data) != nil {
|
} else if (providerConfiguration?[Constants.wireGuardConfigKey] as? Data) != nil {
|
||||||
@@ -104,6 +235,11 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cancelPendingOpenVPNReconnect()
|
||||||
|
cancelPendingNetworkChangeHandling()
|
||||||
|
didReceiveInitialPathUpdate = false
|
||||||
|
updateActiveInterfaceIndexForCurrentPath()
|
||||||
|
|
||||||
switch protoType {
|
switch protoType {
|
||||||
case .wireguard:
|
case .wireguard:
|
||||||
startWireguard(activationAttemptId: activationAttemptId,
|
startWireguard(activationAttemptId: activationAttemptId,
|
||||||
@@ -119,6 +255,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||||||
|
|
||||||
|
|
||||||
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
|
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
|
||||||
|
cancelPendingOpenVPNReconnect()
|
||||||
|
cancelPendingNetworkChangeHandling()
|
||||||
|
|
||||||
guard let protoType else {
|
guard let protoType else {
|
||||||
completionHandler()
|
completionHandler()
|
||||||
return
|
return
|
||||||
@@ -157,25 +296,168 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||||||
of object: Any?,
|
of object: Any?,
|
||||||
change: [NSKeyValueChangeKey: Any]?,
|
change: [NSKeyValueChangeKey: Any]?,
|
||||||
context: UnsafeMutableRawPointer?) {
|
context: UnsafeMutableRawPointer?) {
|
||||||
guard Constants.kDefaultPathKey != keyPath else { return }
|
guard Constants.kDefaultPathKey == keyPath else {
|
||||||
// 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 {
|
|
||||||
return
|
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) {
|
||||||
wg_log(.info, message: "Tunnel restarted.")
|
guard protoType == .xray else {
|
||||||
startTunnel(options: nil, completionHandler: completion)
|
updateActiveInterfaceIndex(for: changePath)
|
||||||
|
completion(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updateActiveInterfaceIndex(for: changePath)
|
||||||
|
reasserting = true
|
||||||
|
xrayLog(.info, message: "Applying network change to xray tunnel")
|
||||||
|
stopXray { }
|
||||||
|
startXray { [weak self] error in
|
||||||
|
self?.reasserting = false
|
||||||
|
completion(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func scheduleNetworkChangeHandling(for proto: TunnelProtoType, path: Network.NWPath) {
|
||||||
|
guard proto == .xray else { return }
|
||||||
|
|
||||||
|
pendingNetworkChangeWorkItem?.cancel()
|
||||||
|
|
||||||
|
let workItem = DispatchWorkItem { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
self.pendingNetworkChangeWorkItem = nil
|
||||||
|
|
||||||
|
if self.isApplyingNetworkChange || self.reasserting {
|
||||||
|
xrayLog(.debug, message: "Skipping network change while restart is already in progress")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isApplyingNetworkChange = true
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.handle(networkChange: path) { [weak self] _ in
|
||||||
|
self?.networkChangeQueue.async {
|
||||||
|
self?.isApplyingNetworkChange = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingNetworkChangeWorkItem = workItem
|
||||||
|
networkChangeQueue.asyncAfter(deadline: .now() + 1.0, execute: workItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func scheduleOpenVPNReconnect(reason: String) {
|
||||||
|
guard protoType == .openvpn else { return }
|
||||||
|
|
||||||
|
pendingOpenVPNReconnectWorkItem?.cancel()
|
||||||
|
|
||||||
|
let workItem = DispatchWorkItem { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
self.pendingOpenVPNReconnectWorkItem = nil
|
||||||
|
|
||||||
|
guard self.protoType == .openvpn else { return }
|
||||||
|
|
||||||
|
if self.reasserting {
|
||||||
|
ovpnLog(.debug, message: "Skipping OpenVPN reconnect while session is already reasserting")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
guard !self.reasserting else {
|
||||||
|
ovpnLog(.debug, message: "Skipping OpenVPN reconnect while session is already reasserting")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ovpnLog(.info, message: "\(reason), reconnecting OpenVPN session")
|
||||||
|
self.ovpnAdapter?.reconnect(afterTimeInterval: 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingOpenVPNReconnectWorkItem = workItem
|
||||||
|
networkChangeQueue.asyncAfter(deadline: .now() + 1.0, execute: workItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleOpenVPNReachabilityChange(_ status: OpenVPNReachabilityStatus) {
|
||||||
|
defer { lastOpenVPNReachabilityStatus = status }
|
||||||
|
|
||||||
|
guard let previousStatus = lastOpenVPNReachabilityStatus else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard previousStatus != status else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch status {
|
||||||
|
case .reachableViaWiFi, .reachableViaWWAN:
|
||||||
|
scheduleOpenVPNReconnect(reason: "Reachability changed")
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func cancelPendingOpenVPNReconnect() {
|
||||||
|
pendingOpenVPNReconnectWorkItem?.cancel()
|
||||||
|
pendingOpenVPNReconnectWorkItem = nil
|
||||||
|
lastOpenVPNReachabilityStatus = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func cancelPendingNetworkChangeHandling() {
|
||||||
|
pendingNetworkChangeWorkItem?.cancel()
|
||||||
|
pendingNetworkChangeWorkItem = nil
|
||||||
|
isApplyingNetworkChange = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
// Ignore loopback and tunnel-style `.other` interfaces so Xray does not
|
||||||
|
// react to its own utun lifecycle as if the physical uplink changed.
|
||||||
|
let preferredTypes: [NWInterface.InterfaceType] = [.wiredEthernet, .wifi, .cellular]
|
||||||
|
let externalInterfaces = path.availableInterfaces.filter { interface in
|
||||||
|
interface.type == .wiredEthernet || interface.type == .wifi || interface.type == .cellular
|
||||||
|
}
|
||||||
|
|
||||||
|
let sortedInterfaces = externalInterfaces.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, .other:
|
||||||
|
continue
|
||||||
|
@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: "|")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,8 +472,112 @@ extension WireGuardLogLevel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NEProviderStopReason: CustomStringConvertible {
|
final class PacketTunnelFlowAdapter: NSObject, OpenVPNAdapterPacketFlow {
|
||||||
public var description: String {
|
private let flow: NEPacketTunnelFlow
|
||||||
|
private var readLogCounter = 0
|
||||||
|
private var writeLogCounter = 0
|
||||||
|
|
||||||
|
init(flow: NEPacketTunnelFlow) {
|
||||||
|
self.flow = flow
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(readPacketsWithCompletionHandler:)
|
||||||
|
func readPackets(completionHandler: @escaping ([Data], [NSNumber]) -> Void) {
|
||||||
|
flow.readPackets { packets, protocols in
|
||||||
|
#if os(macOS)
|
||||||
|
if self.readLogCounter < 20, let firstPacket = packets.first, let firstProtocol = protocols.first {
|
||||||
|
let prefix = firstPacket.prefix(12).map { String(format: "%02x", $0) }.joined()
|
||||||
|
let header = Self.describePacketHeader(firstPacket)
|
||||||
|
ovpnLog(.info, title: "FlowRead", message: "count=\(packets.count) proto0=\(firstProtocol) len0=\(firstPacket.count) prefix=\(prefix) \(header)")
|
||||||
|
self.readLogCounter += 1
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
completionHandler(packets, protocols)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(writePackets:withProtocols:)
|
||||||
|
func writePackets(_ packets: [Data], withProtocols protocols: [NSNumber]) -> Bool {
|
||||||
|
#if os(macOS)
|
||||||
|
if writeLogCounter < 20, let firstPacket = packets.first, let firstProtocol = protocols.first {
|
||||||
|
let prefix = firstPacket.prefix(12).map { String(format: "%02x", $0) }.joined()
|
||||||
|
let header = Self.describePacketHeader(firstPacket)
|
||||||
|
ovpnLog(.info, title: "FlowWrite", message: "count=\(packets.count) proto0=\(firstProtocol) len0=\(firstPacket.count) prefix=\(prefix) \(header)")
|
||||||
|
writeLogCounter += 1
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return flow.writePackets(packets, withProtocols: protocols)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func describePacketHeader(_ packet: Data) -> String {
|
||||||
|
guard let versionNibble = packet.first.map({ Int($0 >> 4) }) else {
|
||||||
|
return "ip=unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
if versionNibble == 4, packet.count >= 20 {
|
||||||
|
let ihl = Int(packet[0] & 0x0f) * 4
|
||||||
|
guard ihl >= 20, packet.count >= ihl else {
|
||||||
|
return "ip=ipv4 malformed"
|
||||||
|
}
|
||||||
|
|
||||||
|
let proto = packet[9]
|
||||||
|
let src = "\(packet[12]).\(packet[13]).\(packet[14]).\(packet[15])"
|
||||||
|
let dst = "\(packet[16]).\(packet[17]).\(packet[18]).\(packet[19])"
|
||||||
|
let l4Offset = ihl
|
||||||
|
let ports: String
|
||||||
|
if (proto == 6 || proto == 17) && packet.count >= l4Offset + 4 {
|
||||||
|
let srcPort = (UInt16(packet[l4Offset]) << 8) | UInt16(packet[l4Offset + 1])
|
||||||
|
let dstPort = (UInt16(packet[l4Offset + 2]) << 8) | UInt16(packet[l4Offset + 3])
|
||||||
|
ports = "sport=\(srcPort) dport=\(dstPort)"
|
||||||
|
} else {
|
||||||
|
ports = "sport=- dport=-"
|
||||||
|
}
|
||||||
|
let protoName: String
|
||||||
|
switch proto {
|
||||||
|
case 1: protoName = "ICMP"
|
||||||
|
case 6: protoName = "TCP"
|
||||||
|
case 17: protoName = "UDP"
|
||||||
|
default: protoName = "P\(proto)"
|
||||||
|
}
|
||||||
|
return "ip=ipv4 src=\(src) dst=\(dst) proto=\(protoName) \(ports)"
|
||||||
|
}
|
||||||
|
|
||||||
|
if versionNibble == 6, packet.count >= 40 {
|
||||||
|
let proto = packet[6]
|
||||||
|
func hex16(_ start: Int) -> String {
|
||||||
|
let value = (UInt16(packet[start]) << 8) | UInt16(packet[start + 1])
|
||||||
|
return String(format: "%x", value)
|
||||||
|
}
|
||||||
|
let src = stride(from: 8, to: 24, by: 2).map(hex16).joined(separator: ":")
|
||||||
|
let dst = stride(from: 24, to: 40, by: 2).map(hex16).joined(separator: ":")
|
||||||
|
let l4Offset = 40
|
||||||
|
let ports: String
|
||||||
|
if (proto == 6 || proto == 17) && packet.count >= l4Offset + 4 {
|
||||||
|
let srcPort = (UInt16(packet[l4Offset]) << 8) | UInt16(packet[l4Offset + 1])
|
||||||
|
let dstPort = (UInt16(packet[l4Offset + 2]) << 8) | UInt16(packet[l4Offset + 3])
|
||||||
|
ports = "sport=\(srcPort) dport=\(dstPort)"
|
||||||
|
} else {
|
||||||
|
ports = "sport=- dport=-"
|
||||||
|
}
|
||||||
|
let protoName: String
|
||||||
|
switch proto {
|
||||||
|
case 58: protoName = "ICMPv6"
|
||||||
|
case 6: protoName = "TCP"
|
||||||
|
case 17: protoName = "UDP"
|
||||||
|
default: protoName = "P\(proto)"
|
||||||
|
}
|
||||||
|
return "ip=ipv6 src=\(src) dst=\(dst) proto=\(protoName) \(ports)"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "ip=v\(versionNibble) len=\(packet.count)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NEPacketTunnelFlow: OpenVPNAdapterPacketFlow {}
|
||||||
|
|
||||||
|
extension NEProviderStopReason {
|
||||||
|
var amneziaDescription: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .none:
|
case .none:
|
||||||
return "No specific reason"
|
return "No specific reason"
|
||||||
@@ -223,6 +609,8 @@ extension NEProviderStopReason: CustomStringConvertible {
|
|||||||
return "The current console user changed"
|
return "The current console user changed"
|
||||||
case .connectionFailed:
|
case .connectionFailed:
|
||||||
return "The connection failed"
|
return "The connection failed"
|
||||||
|
case .internalError:
|
||||||
|
return "The network extension reported an internal error"
|
||||||
case .sleep:
|
case .sleep:
|
||||||
return "A stop reason indicating the VPNC enabled disconnect on sleep and the device went to sleep"
|
return "A stop reason indicating the VPNC enabled disconnect on sleep and the device went to sleep"
|
||||||
case .appUpdate:
|
case .appUpdate:
|
||||||
|
|||||||
@@ -11,13 +11,7 @@ class ScreenProtection {
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
public func toggleScreenshots(_ isEnabled: Bool) {
|
public func toggleScreenshots(_ isEnabled: Bool) {
|
||||||
let window = UIApplication.shared.keyWindows.first!
|
ScreenProtection.shared.setScreenshotsEnabled(isEnabled)
|
||||||
|
|
||||||
if isEnabled {
|
|
||||||
ScreenProtection.shared.disable(for: window.rootViewController!.view)
|
|
||||||
} else {
|
|
||||||
ScreenProtection.shared.enable(for: window.rootViewController!.view)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension UIApplication {
|
extension UIApplication {
|
||||||
@@ -45,6 +39,45 @@ class ScreenProtection {
|
|||||||
|
|
||||||
private var blurView: UIVisualEffectView?
|
private var blurView: UIVisualEffectView?
|
||||||
private var recordingObservation: NSKeyValueObservation?
|
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) {
|
public func enable(for view: UIView) {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import Foundation
|
||||||
|
import StoreKit
|
||||||
|
|
||||||
|
@available(iOS 15.0, macOS 12.0, *)
|
||||||
|
@objcMembers
|
||||||
|
public class StoreKit2Helper: NSObject {
|
||||||
|
|
||||||
|
public static let shared = StoreKit2Helper()
|
||||||
|
private static let errorDomain = "StoreKit2Helper"
|
||||||
|
|
||||||
|
private struct EntitlementInfo {
|
||||||
|
let transactionId: UInt64
|
||||||
|
let originalTransactionId: UInt64
|
||||||
|
let productId: String
|
||||||
|
let purchaseDate: Date
|
||||||
|
|
||||||
|
var dictionary: NSDictionary {
|
||||||
|
[
|
||||||
|
"transactionId": String(transactionId),
|
||||||
|
"originalTransactionId": String(originalTransactionId),
|
||||||
|
"productId": productId
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func fetchCurrentEntitlements(completion: @escaping (Bool, [NSDictionary]?, NSError?) -> Void) {
|
||||||
|
Task { @MainActor in
|
||||||
|
do {
|
||||||
|
try await AppStore.sync()
|
||||||
|
|
||||||
|
var entitlements: [EntitlementInfo] = []
|
||||||
|
for await result in Transaction.currentEntitlements {
|
||||||
|
switch result {
|
||||||
|
case .verified(let transaction):
|
||||||
|
entitlements.append(EntitlementInfo(transactionId: transaction.id,
|
||||||
|
originalTransactionId: transaction.originalID,
|
||||||
|
productId: transaction.productID,
|
||||||
|
purchaseDate: transaction.purchaseDate))
|
||||||
|
case .unverified(_, let error):
|
||||||
|
print("[IAP][StoreKit2] Unverified transaction skipped: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let sortedEntitlements = entitlements.sorted { lhs, rhs in
|
||||||
|
if lhs.purchaseDate != rhs.purchaseDate {
|
||||||
|
return lhs.purchaseDate > rhs.purchaseDate
|
||||||
|
}
|
||||||
|
return lhs.transactionId > rhs.transactionId
|
||||||
|
}.map { $0.dictionary }
|
||||||
|
completion(true, sortedEntitlements, nil)
|
||||||
|
} catch {
|
||||||
|
completion(false, nil, error as NSError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func purchaseProduct(productIdentifier: String, completion: @escaping (Bool, String?, String?, String?, NSError?) -> Void) {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let products = try await Product.products(for: [productIdentifier])
|
||||||
|
guard let product = products.first else {
|
||||||
|
completePurchase(completion: completion, success: false, transactionId: nil, productId: nil, originalTransactionId: nil,
|
||||||
|
error: makeError(code: 0, description: "Product not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let result = try await product.purchase()
|
||||||
|
switch result {
|
||||||
|
case .success(let verification):
|
||||||
|
switch verification {
|
||||||
|
case .verified(let transaction):
|
||||||
|
await transaction.finish()
|
||||||
|
completePurchase(completion: completion, success: true, transactionId: String(transaction.id),
|
||||||
|
productId: transaction.productID, originalTransactionId: String(transaction.originalID), error: nil)
|
||||||
|
case .unverified(_, let error):
|
||||||
|
completePurchase(completion: completion, success: false, transactionId: nil, productId: nil, originalTransactionId: nil,
|
||||||
|
error: error as NSError)
|
||||||
|
}
|
||||||
|
case .userCancelled:
|
||||||
|
completePurchase(completion: completion, success: false, transactionId: nil, productId: nil, originalTransactionId: nil,
|
||||||
|
error: makeError(code: 1, description: "Purchase cancelled"))
|
||||||
|
case .pending:
|
||||||
|
completePurchase(completion: completion, success: false, transactionId: nil, productId: nil, originalTransactionId: nil,
|
||||||
|
error: makeError(code: 2, description: "Purchase pending"))
|
||||||
|
@unknown default:
|
||||||
|
completePurchase(completion: completion, success: false, transactionId: nil, productId: nil, originalTransactionId: nil,
|
||||||
|
error: makeError(code: 3, description: "Unknown purchase result"))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
completePurchase(completion: completion, success: false, transactionId: nil, productId: nil, originalTransactionId: nil,
|
||||||
|
error: error as NSError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func storefrontCurrencyCode(for product: Product) -> String {
|
||||||
|
product.priceFormatStyle.locale.currencyCode ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
private func subscriptionBillingMonths(_ period: Product.SubscriptionPeriod) -> Double {
|
||||||
|
let periodValue = Double(period.value)
|
||||||
|
switch period.unit {
|
||||||
|
case .day:
|
||||||
|
return periodValue / 30.0
|
||||||
|
case .week:
|
||||||
|
return periodValue * 7.0 / 30.0
|
||||||
|
case .month:
|
||||||
|
return periodValue
|
||||||
|
case .year:
|
||||||
|
return periodValue * 12.0
|
||||||
|
@unknown default:
|
||||||
|
return periodValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func fetchProducts(identifiers: Set<String>, completion: @escaping ([NSDictionary], [String], NSError?) -> Void) {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let products = try await Product.products(for: identifiers)
|
||||||
|
let productDicts = products.map { product in productDictionary(for: product) }
|
||||||
|
let fetchedIds = Set(products.map { $0.id })
|
||||||
|
let invalidIdentifiers = identifiers.filter { !fetchedIds.contains($0) }
|
||||||
|
DispatchQueue.main.async { completion(productDicts, Array(invalidIdentifiers), nil) }
|
||||||
|
} catch {
|
||||||
|
DispatchQueue.main.async { completion([], Array(identifiers), error as NSError) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeError(code: Int, description: String) -> NSError {
|
||||||
|
NSError(domain: Self.errorDomain, code: code, userInfo: [NSLocalizedDescriptionKey: description])
|
||||||
|
}
|
||||||
|
|
||||||
|
private func completePurchase(completion: @escaping (Bool, String?, String?, String?, NSError?) -> Void,
|
||||||
|
success: Bool,
|
||||||
|
transactionId: String?,
|
||||||
|
productId: String?,
|
||||||
|
originalTransactionId: String?,
|
||||||
|
error: NSError?) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(success, transactionId, productId, originalTransactionId, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func productDictionary(for product: Product) -> NSDictionary {
|
||||||
|
let currencyCode = storefrontCurrencyCode(for: product)
|
||||||
|
var productData: [String: Any] = [
|
||||||
|
"productId": product.id,
|
||||||
|
"title": product.displayName,
|
||||||
|
"description": product.description,
|
||||||
|
"price": "\(product.price)",
|
||||||
|
"displayPrice": product.displayPrice,
|
||||||
|
"currencyCode": currencyCode,
|
||||||
|
"priceAmount": NSDecimalNumber(decimal: product.price).doubleValue
|
||||||
|
]
|
||||||
|
if let subscription = product.subscription {
|
||||||
|
let billingMonths = subscriptionBillingMonths(subscription.subscriptionPeriod)
|
||||||
|
productData["subscriptionBillingMonths"] = billingMonths
|
||||||
|
if let perMonthPrice = displayPricePerMonth(for: product, billingMonths: billingMonths, currencyCode: currencyCode) {
|
||||||
|
productData["displayPricePerMonth"] = perMonthPrice
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return productData as NSDictionary
|
||||||
|
}
|
||||||
|
|
||||||
|
private func displayPricePerMonth(for product: Product, billingMonths: Double, currencyCode: String) -> String? {
|
||||||
|
if billingMonths <= 1e-6 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let perMonthPrice = product.price / Decimal(billingMonths)
|
||||||
|
let formatter = NumberFormatter()
|
||||||
|
formatter.numberStyle = .currency
|
||||||
|
formatter.locale = product.priceFormatStyle.locale
|
||||||
|
if !currencyCode.isEmpty {
|
||||||
|
formatter.currencyCode = currencyCode
|
||||||
|
}
|
||||||
|
return formatter.string(from: NSDecimalNumber(decimal: perMonthPrice))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,114 @@
|
|||||||
|
/* 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>
|
||||||
|
#import <AmneziaVPN-Swift.h>
|
||||||
|
|
||||||
|
#include <QtCore/QDebug>
|
||||||
|
#include <QtCore/QString>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
QString toQString(NSString *value)
|
||||||
|
{
|
||||||
|
return QString::fromUtf8((value ?: @"").UTF8String);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
API_AVAILABLE(ios(15.0), macos(12.0))
|
||||||
|
@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];
|
||||||
|
return 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))
|
||||||
|
{
|
||||||
|
qInfo().noquote() << "[IAP][StoreKit2] Starting purchase for" << QString::fromUtf8(productIdentifier.UTF8String);
|
||||||
|
[[StoreKit2Helper shared] purchaseProductWithProductIdentifier:productIdentifier
|
||||||
|
completion:^(BOOL success,
|
||||||
|
NSString *transactionId,
|
||||||
|
NSString *productId,
|
||||||
|
NSString *originalTransactionId,
|
||||||
|
NSError *error) {
|
||||||
|
if (success) {
|
||||||
|
qInfo().noquote() << "[IAP][StoreKit2] Purchase success. transactionId =" << toQString(transactionId)
|
||||||
|
<< "originalTransactionId =" << toQString(originalTransactionId) << "productId =" << toQString(productId);
|
||||||
|
} else if (error) {
|
||||||
|
qWarning().noquote() << "[IAP][StoreKit2] Purchase failed:" << toQString(error.localizedDescription);
|
||||||
|
}
|
||||||
|
if (completion) {
|
||||||
|
completion(success, transactionId, productId, originalTransactionId, error);
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)restorePurchasesWithCompletion:(void (^)(BOOL success,
|
||||||
|
NSArray<NSDictionary *> *_Nullable restoredTransactions,
|
||||||
|
NSError *_Nullable error))completion API_AVAILABLE(ios(15.0), macos(12.0))
|
||||||
|
{
|
||||||
|
[[StoreKit2Helper shared] fetchCurrentEntitlementsWithCompletion:^(BOOL success,
|
||||||
|
NSArray<NSDictionary *> *entitlements,
|
||||||
|
NSError *error) {
|
||||||
|
if (success) {
|
||||||
|
qInfo().noquote() << "[IAP][StoreKit2] currentEntitlements returned"
|
||||||
|
<< (int)(entitlements ? entitlements.count : 0) << "active entitlements";
|
||||||
|
for (NSDictionary *entitlement in entitlements) {
|
||||||
|
qInfo().noquote() << "[IAP][StoreKit2] Active entitlement:"
|
||||||
|
<< "transactionId=" << toQString(entitlement[@"transactionId"])
|
||||||
|
<< "originalTransactionId=" << toQString(entitlement[@"originalTransactionId"])
|
||||||
|
<< "productId=" << toQString(entitlement[@"productId"]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
qWarning().noquote() << "[IAP][StoreKit2] fetchCurrentEntitlements failed:" << toQString(error.localizedDescription);
|
||||||
|
}
|
||||||
|
if (completion) {
|
||||||
|
completion(success, entitlements, error);
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (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))
|
||||||
|
{
|
||||||
|
[[StoreKit2Helper shared] fetchProductsWithIdentifiers:productIdentifiers
|
||||||
|
completion:^(NSArray<NSDictionary *> *products,
|
||||||
|
NSArray<NSString *> *invalidIdentifiers,
|
||||||
|
NSError *error) {
|
||||||
|
if (!error) {
|
||||||
|
for (NSDictionary *productInfo in products) {
|
||||||
|
qInfo().noquote() << "[IAP][StoreKit2] Fetched product info" << toQString(productInfo[@"productId"])
|
||||||
|
<< "price=" << toQString(productInfo[@"price"])
|
||||||
|
<< "currency=" << toQString(productInfo[@"currencyCode"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (completion) {
|
||||||
|
completion(products ?: @[], invalidIdentifiers ?: @[], error);
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -6,8 +6,6 @@ struct WGConfig: Decodable {
|
|||||||
let junkPacketCount, junkPacketMinSize, junkPacketMaxSize: String?
|
let junkPacketCount, junkPacketMinSize, junkPacketMaxSize: String?
|
||||||
let initPacketJunkSize, responsePacketJunkSize, cookieReplyPacketJunkSize, transportPacketJunkSize: String?
|
let initPacketJunkSize, responsePacketJunkSize, cookieReplyPacketJunkSize, transportPacketJunkSize: String?
|
||||||
let specialJunk1, specialJunk2, specialJunk3, specialJunk4, specialJunk5: String?
|
let specialJunk1, specialJunk2, specialJunk3, specialJunk4, specialJunk5: String?
|
||||||
let controlledJunk1, controlledJunk2, controlledJunk3: String?
|
|
||||||
let specialHandshakeTimeout: String?
|
|
||||||
let dns1: String
|
let dns1: String
|
||||||
let dns2: String
|
let dns2: String
|
||||||
let mtu: String
|
let mtu: String
|
||||||
@@ -28,8 +26,6 @@ struct WGConfig: Decodable {
|
|||||||
case junkPacketCount = "Jc", junkPacketMinSize = "Jmin", junkPacketMaxSize = "Jmax"
|
case junkPacketCount = "Jc", junkPacketMinSize = "Jmin", junkPacketMaxSize = "Jmax"
|
||||||
case initPacketJunkSize = "S1", responsePacketJunkSize = "S2", cookieReplyPacketJunkSize = "S3", transportPacketJunkSize = "S4"
|
case initPacketJunkSize = "S1", responsePacketJunkSize = "S2", cookieReplyPacketJunkSize = "S3", transportPacketJunkSize = "S4"
|
||||||
case specialJunk1 = "I1", specialJunk2 = "I2", specialJunk3 = "I3", specialJunk4 = "I4", specialJunk5 = "I5"
|
case specialJunk1 = "I1", specialJunk2 = "I2", specialJunk3 = "I3", specialJunk4 = "I4", specialJunk5 = "I5"
|
||||||
case controlledJunk1 = "J1", controlledJunk2 = "J2", controlledJunk3 = "J3"
|
|
||||||
case specialHandshakeTimeout = "Itime"
|
|
||||||
case dns1
|
case dns1
|
||||||
case dns2
|
case dns2
|
||||||
case mtu
|
case mtu
|
||||||
@@ -46,58 +42,64 @@ struct WGConfig: Decodable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var settings: String {
|
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] = []
|
var settingsLines: [String] = []
|
||||||
|
|
||||||
// Required parameters when junkPacketCount is present
|
// Required parameters when junkPacketCount is present
|
||||||
settingsLines.append("Jc = \(junkPacketCount!)")
|
settingsLines.append("Jc = \(junkPacketCount)")
|
||||||
settingsLines.append("Jmin = \(junkPacketMinSize!)")
|
settingsLines.append("Jmin = \(junkPacketMinSize)")
|
||||||
settingsLines.append("Jmax = \(junkPacketMaxSize!)")
|
settingsLines.append("Jmax = \(junkPacketMaxSize)")
|
||||||
settingsLines.append("S1 = \(initPacketJunkSize!)")
|
settingsLines.append("S1 = \(initPacketJunkSize)")
|
||||||
settingsLines.append("S2 = \(responsePacketJunkSize!)")
|
settingsLines.append("S2 = \(responsePacketJunkSize)")
|
||||||
|
|
||||||
settingsLines.append("H1 = \(initPacketMagicHeader!)")
|
settingsLines.append("H1 = \(initPacketMagicHeader)")
|
||||||
settingsLines.append("H2 = \(responsePacketMagicHeader!)")
|
settingsLines.append("H2 = \(responsePacketMagicHeader)")
|
||||||
settingsLines.append("H3 = \(underloadPacketMagicHeader!)")
|
settingsLines.append("H3 = \(underloadPacketMagicHeader)")
|
||||||
settingsLines.append("H4 = \(transportPacketMagicHeader!)")
|
settingsLines.append("H4 = \(transportPacketMagicHeader)")
|
||||||
|
|
||||||
// Optional parameters - only add if not nil and not empty
|
// 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)")
|
settingsLines.append("S3 = \(s3)")
|
||||||
}
|
}
|
||||||
if let s4 = transportPacketJunkSize, !s4.isEmpty {
|
if let s4 = trimmed(transportPacketJunkSize) {
|
||||||
settingsLines.append("S4 = \(s4)")
|
settingsLines.append("S4 = \(s4)")
|
||||||
}
|
}
|
||||||
|
|
||||||
if let i1 = specialJunk1, !i1.isEmpty {
|
if let i1 = trimmed(specialJunk1) {
|
||||||
settingsLines.append("I1 = \(i1)")
|
settingsLines.append("I1 = \(i1)")
|
||||||
}
|
}
|
||||||
if let i2 = specialJunk2, !i2.isEmpty {
|
if let i2 = trimmed(specialJunk2) {
|
||||||
settingsLines.append("I2 = \(i2)")
|
settingsLines.append("I2 = \(i2)")
|
||||||
}
|
}
|
||||||
if let i3 = specialJunk3, !i3.isEmpty {
|
if let i3 = trimmed(specialJunk3) {
|
||||||
settingsLines.append("I3 = \(i3)")
|
settingsLines.append("I3 = \(i3)")
|
||||||
}
|
}
|
||||||
if let i4 = specialJunk4, !i4.isEmpty {
|
if let i4 = trimmed(specialJunk4) {
|
||||||
settingsLines.append("I4 = \(i4)")
|
settingsLines.append("I4 = \(i4)")
|
||||||
}
|
}
|
||||||
if let i5 = specialJunk5, !i5.isEmpty {
|
if let i5 = trimmed(specialJunk5) {
|
||||||
settingsLines.append("I5 = \(i5)")
|
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")
|
return settingsLines.joined(separator: "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ import Foundation
|
|||||||
struct XrayConfig: Decodable {
|
struct XrayConfig: Decodable {
|
||||||
let dns1: String?
|
let dns1: String?
|
||||||
let dns2: String?
|
let dns2: String?
|
||||||
|
let splitTunnelType: Int?
|
||||||
|
let splitTunnelSites: [String]?
|
||||||
let config: String
|
let config: String
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
#define IOS_CONTROLLER_H
|
#define IOS_CONTROLLER_H
|
||||||
|
|
||||||
#include "protocols/vpnprotocol.h"
|
#include "protocols/vpnprotocol.h"
|
||||||
|
#include <functional>
|
||||||
|
#include <QVariant>
|
||||||
|
#include <QVariantMap>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QList>
|
||||||
|
#include <QElapsedTimer>
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
#ifdef __OBJC__
|
#ifdef __OBJC__
|
||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
@@ -55,7 +62,24 @@ public:
|
|||||||
bool shareText(const QStringList &filesToSend);
|
bool shareText(const QStringList &filesToSend);
|
||||||
QString openFile();
|
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();
|
void requestInetAccess();
|
||||||
|
bool isTestFlight();
|
||||||
signals:
|
signals:
|
||||||
void connectionStateChanged(Vpn::ConnectionState state);
|
void connectionStateChanged(Vpn::ConnectionState state);
|
||||||
void bytesChanged(quint64 receivedBytes, quint64 sentBytes);
|
void bytesChanged(quint64 receivedBytes, quint64 sentBytes);
|
||||||
@@ -81,6 +105,7 @@ private:
|
|||||||
bool startXray(const QString &jsonConfig);
|
bool startXray(const QString &jsonConfig);
|
||||||
|
|
||||||
void startTunnel();
|
void startTunnel();
|
||||||
|
void emitConnectionStateIfChanged(Vpn::ConnectionState state);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void *m_iosControllerWrapper {};
|
void *m_iosControllerWrapper {};
|
||||||
@@ -94,8 +119,13 @@ private:
|
|||||||
amnezia::Proto m_proto;
|
amnezia::Proto m_proto;
|
||||||
QJsonObject m_rawConfig;
|
QJsonObject m_rawConfig;
|
||||||
QString m_tunnelId;
|
QString m_tunnelId;
|
||||||
uint64_t m_txBytes;
|
uint64_t m_txBytes = 0;
|
||||||
uint64_t m_rxBytes;
|
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
|
#endif // IOS_CONTROLLER_H
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
#include "../protocols/vpnprotocol.h"
|
#include "../protocols/vpnprotocol.h"
|
||||||
#import "ios_controller_wrapper.h"
|
#import "ios_controller_wrapper.h"
|
||||||
|
#import "StoreKitController.h"
|
||||||
|
|
||||||
const char* Action::start = "start";
|
const char* Action::start = "start";
|
||||||
const char* Action::restart = "restart";
|
const char* Action::restart = "restart";
|
||||||
@@ -92,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 {
|
namespace {
|
||||||
IosController* s_instance = nullptr;
|
IosController* s_instance = nullptr;
|
||||||
}
|
}
|
||||||
@@ -101,6 +144,9 @@ IosController::IosController() : QObject()
|
|||||||
s_instance = this;
|
s_instance = this;
|
||||||
m_iosControllerWrapper = [[IosControllerWrapper alloc] initWithCppController:this];
|
m_iosControllerWrapper = [[IosControllerWrapper alloc] initWithCppController:this];
|
||||||
|
|
||||||
|
// Initialize StoreKitController early to start observing the payment queue
|
||||||
|
[StoreKitController sharedInstance];
|
||||||
|
|
||||||
[[NSNotificationCenter defaultCenter]
|
[[NSNotificationCenter defaultCenter]
|
||||||
removeObserver: (__bridge NSObject *)m_iosControllerWrapper];
|
removeObserver: (__bridge NSObject *)m_iosControllerWrapper];
|
||||||
[[NSNotificationCenter defaultCenter]
|
[[NSNotificationCenter defaultCenter]
|
||||||
@@ -110,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() {
|
IosController* IosController::Instance() {
|
||||||
if (!s_instance) {
|
if (!s_instance) {
|
||||||
s_instance = new IosController();
|
s_instance = new IosController();
|
||||||
@@ -124,8 +179,9 @@ bool IosController::initialize()
|
|||||||
[NETunnelProviderManager loadAllFromPreferencesWithCompletionHandler:^(NSArray<NETunnelProviderManager *> * _Nullable managers, NSError * _Nullable error) {
|
[NETunnelProviderManager loadAllFromPreferencesWithCompletionHandler:^(NSArray<NETunnelProviderManager *> * _Nullable managers, NSError * _Nullable error) {
|
||||||
@try {
|
@try {
|
||||||
if (error) {
|
if (error) {
|
||||||
qDebug() << "IosController::initialize : Error:" << [error.localizedDescription UTF8String];
|
qWarning() << "IosController::initialize : loadAllFromPreferences failed:"
|
||||||
emit connectionStateChanged(Vpn::ConnectionState::Error);
|
<< [error.localizedDescription UTF8String]
|
||||||
|
<< "domain:" << [error.domain UTF8String] << "code:" << error.code;
|
||||||
ok = false;
|
ok = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -162,16 +218,13 @@ bool IosController::connectVpn(amnezia::Proto proto, const QJsonObject& configur
|
|||||||
m_rawConfig = configuration;
|
m_rawConfig = configuration;
|
||||||
m_serverAddress = configuration.value(config_key::hostName).toString().toNSString();
|
m_serverAddress = configuration.value(config_key::hostName).toString().toNSString();
|
||||||
|
|
||||||
|
const QString serverDescription = configuration.value(config_key::description).toString().trimmed();
|
||||||
QString tunnelName;
|
QString tunnelName;
|
||||||
if (configuration.value(config_key::description).toString().isEmpty()) {
|
if (serverDescription.isEmpty()) {
|
||||||
|
tunnelName = ProtocolProps::protoToString(proto);
|
||||||
|
} else {
|
||||||
tunnelName = QString("%1 %2")
|
tunnelName = QString("%1 %2")
|
||||||
.arg(configuration.value(config_key::hostName).toString())
|
.arg(serverDescription)
|
||||||
.arg(ProtocolProps::protoToString(proto));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
tunnelName = QString("%1 (%2) %3")
|
|
||||||
.arg(configuration.value(config_key::description).toString())
|
|
||||||
.arg(configuration.value(config_key::hostName).toString())
|
|
||||||
.arg(ProtocolProps::protoToString(proto));
|
.arg(ProtocolProps::protoToString(proto));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,33 +329,65 @@ void IosController::disconnectVpn()
|
|||||||
|
|
||||||
void IosController::checkStatus()
|
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 *actionKey = [NSString stringWithUTF8String:MessageKey::action];
|
||||||
NSString *actionValue = [NSString stringWithUTF8String:Action::getStatus];
|
NSString *actionValue = [NSString stringWithUTF8String:Action::getStatus];
|
||||||
NSString *tunnelIdKey = [NSString stringWithUTF8String:MessageKey::tunnelId];
|
NSString *tunnelIdKey = [NSString stringWithUTF8String:MessageKey::tunnelId];
|
||||||
NSString *tunnelIdValue = !m_tunnelId.isEmpty() ? m_tunnelId.toNSString() : @"";
|
NSString *tunnelIdValue = !m_tunnelId.isEmpty() ? m_tunnelId.toNSString() : @"";
|
||||||
|
|
||||||
NSDictionary* message = @{actionKey: actionValue, tunnelIdKey: tunnelIdValue};
|
NSDictionary* message = @{actionKey: actionValue, tunnelIdKey: tunnelIdValue};
|
||||||
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||||
sendVpnExtensionMessage(message, [&](NSDictionary* response){
|
sendVpnExtensionMessage(message, [&](NSDictionary* response){
|
||||||
uint64_t txBytes = [response[@"tx_bytes"] intValue];
|
if (!response) {
|
||||||
uint64_t rxBytes = [response[@"rx_bytes"] intValue];
|
QMetaObject::invokeMethod(this, [this]() {
|
||||||
|
m_statusRequestInFlight = false;
|
||||||
uint64_t last_handshake_time_sec = 0;
|
}, Qt::QueuedConnection);
|
||||||
#if !MACOS_NE
|
return;
|
||||||
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 (last_handshake_time_sec < 0) {
|
const uint64_t txBytes = uint64FromResponse(response, @"tx_bytes");
|
||||||
disconnectVpn();
|
const uint64_t rxBytes = uint64FromResponse(response, @"rx_bytes");
|
||||||
qDebug() << "Invalid handshake time, disconnecting VPN.";
|
const long long last_handshake_time_sec = int64FromResponse(response, @"last_handshake_time_sec");
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
emit bytesChanged(rxBytes - m_rxBytes, txBytes - m_txBytes);
|
QMetaObject::invokeMethod(this, [this, txBytes, rxBytes, last_handshake_time_sec]() {
|
||||||
m_rxBytes = rxBytes;
|
if (isWireGuardBasedProto(m_proto) && m_handshakeAwaiting) {
|
||||||
m_txBytes = txBytes;
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,8 +395,14 @@ void IosController::vpnStatusDidChange(void *pNotification)
|
|||||||
{
|
{
|
||||||
NETunnelProviderSession *session = (NETunnelProviderSession *)pNotification;
|
NETunnelProviderSession *session = (NETunnelProviderSession *)pNotification;
|
||||||
|
|
||||||
if (session /* && session == TunnelManager.session */ ) {
|
if (!session) {
|
||||||
qDebug() << "IosController::vpnStatusDidChange" << iosStatusToState(session.status) << session;
|
return;
|
||||||
|
}
|
||||||
|
if (!m_currentTunnel || (NETunnelProviderSession *)m_currentTunnel.connection != session) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "IosController::vpnStatusDidChange" << iosStatusToState(session.status) << session;
|
||||||
|
|
||||||
if (session.status == NEVPNStatusDisconnected) {
|
if (session.status == NEVPNStatusDisconnected) {
|
||||||
if (@available(iOS 16.0, *)) {
|
if (@available(iOS 16.0, *)) {
|
||||||
@@ -409,8 +500,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
void IosController::vpnConfigurationDidChange(void *pNotification)
|
void IosController::vpnConfigurationDidChange(void *pNotification)
|
||||||
@@ -444,6 +549,16 @@ bool IosController::setupOpenVPN()
|
|||||||
|
|
||||||
QJsonDocument openVPNConfigDoc(openVPNConfig);
|
QJsonDocument openVPNConfigDoc(openVPNConfig);
|
||||||
QString openVPNConfigStr(openVPNConfigDoc.toJson(QJsonDocument::Compact));
|
QString openVPNConfigStr(openVPNConfigDoc.toJson(QJsonDocument::Compact));
|
||||||
|
QString openVPNConfigPreview = openVPNConfigStr.left(512);
|
||||||
|
QString ovpnPreview = ovpnConfig.left(512);
|
||||||
|
|
||||||
|
qDebug().noquote() << "IosController::setupOpenVPN payload"
|
||||||
|
<< "jsonBytes=" << openVPNConfigStr.toUtf8().size()
|
||||||
|
<< "ovpnChars=" << ovpnConfig.size()
|
||||||
|
<< "splitTunnelType=" << m_rawConfig[config_key::splitTunnelType].toInt()
|
||||||
|
<< "splitTunnelSites=" << splitTunnelSites;
|
||||||
|
qDebug().noquote() << "IosController::setupOpenVPN payload jsonPreview=" << openVPNConfigPreview;
|
||||||
|
qDebug().noquote() << "IosController::setupOpenVPN payload ovpnPreview=" << ovpnPreview;
|
||||||
|
|
||||||
return startOpenVPN(openVPNConfigStr);
|
return startOpenVPN(openVPNConfigStr);
|
||||||
}
|
}
|
||||||
@@ -582,6 +697,15 @@ bool IosController::setupXray()
|
|||||||
QJsonObject finalConfig;
|
QJsonObject finalConfig;
|
||||||
finalConfig.insert(config_key::dns1, m_rawConfig[config_key::dns1].toString());
|
finalConfig.insert(config_key::dns1, m_rawConfig[config_key::dns1].toString());
|
||||||
finalConfig.insert(config_key::dns2, m_rawConfig[config_key::dns2].toString());
|
finalConfig.insert(config_key::dns2, m_rawConfig[config_key::dns2].toString());
|
||||||
|
finalConfig.insert(config_key::splitTunnelType, m_rawConfig[config_key::splitTunnelType]);
|
||||||
|
|
||||||
|
QJsonArray splitTunnelSites = m_rawConfig[config_key::splitTunnelSites].toArray();
|
||||||
|
|
||||||
|
for(int index = 0; index < splitTunnelSites.count(); index++) {
|
||||||
|
splitTunnelSites[index] = splitTunnelSites[index].toString().remove(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
finalConfig.insert(config_key::splitTunnelSites, splitTunnelSites);
|
||||||
finalConfig.insert(config_key::config, xrayConfigStr);
|
finalConfig.insert(config_key::config, xrayConfigStr);
|
||||||
|
|
||||||
QJsonDocument finalConfigDoc(finalConfig);
|
QJsonDocument finalConfigDoc(finalConfig);
|
||||||
@@ -670,10 +794,6 @@ bool IosController::setupAwg()
|
|||||||
wgConfig.insert(config_key::specialJunk3, config[config_key::specialJunk3]);
|
wgConfig.insert(config_key::specialJunk3, config[config_key::specialJunk3]);
|
||||||
wgConfig.insert(config_key::specialJunk4, config[config_key::specialJunk4]);
|
wgConfig.insert(config_key::specialJunk4, config[config_key::specialJunk4]);
|
||||||
wgConfig.insert(config_key::specialJunk5, config[config_key::specialJunk5]);
|
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);
|
QJsonDocument wgConfigDoc(wgConfig);
|
||||||
QString wgConfigDocStr(wgConfigDoc.toJson(QJsonDocument::Compact));
|
QString wgConfigDocStr(wgConfigDoc.toJson(QJsonDocument::Compact));
|
||||||
@@ -687,11 +807,59 @@ bool IosController::startOpenVPN(const QString &config)
|
|||||||
|
|
||||||
NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init];
|
NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init];
|
||||||
tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID];
|
tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID];
|
||||||
tunnelProtocol.providerConfiguration = @{@"ovpn": [[NSString stringWithUTF8String:config.toStdString().c_str()] dataUsingEncoding:NSUTF8StringEncoding]};
|
QByteArray configUtf8 = config.toUtf8();
|
||||||
|
NSData *ovpnConfigData = [NSData dataWithBytes:configUtf8.constData() length:configUtf8.size()];
|
||||||
|
tunnelProtocol.providerConfiguration = @{@"ovpn": ovpnConfigData};
|
||||||
tunnelProtocol.serverAddress = m_serverAddress;
|
tunnelProtocol.serverAddress = m_serverAddress;
|
||||||
|
if (@available(iOS 14.0, macOS 11.0, *)) {
|
||||||
|
int splitTunnelType = 0;
|
||||||
|
QJsonParseError parseError;
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(config.toUtf8(), &parseError);
|
||||||
|
if (parseError.error == QJsonParseError::NoError && doc.isObject()) {
|
||||||
|
QJsonObject obj = doc.object();
|
||||||
|
splitTunnelType = obj.value(config_key::splitTunnelType).toInt(0);
|
||||||
|
}
|
||||||
|
#if defined(MACOS_NE)
|
||||||
|
// On macOS NE use route-based full tunnel. includeAllNetworks enables
|
||||||
|
// policy-based drop-all mode and causes enforceRoutes to be ignored.
|
||||||
|
tunnelProtocol.includeAllNetworks = NO;
|
||||||
|
if (splitTunnelType == 0) {
|
||||||
|
tunnelProtocol.enforceRoutes = YES;
|
||||||
|
if (@available(iOS 14.2, macOS 11.0, *)) {
|
||||||
|
tunnelProtocol.excludeLocalNetworks = YES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
tunnelProtocol.includeAllNetworks = (splitTunnelType == 0);
|
||||||
|
if (@available(iOS 14.2, macOS 11.0, *)) {
|
||||||
|
// Keep existing iOS behavior.
|
||||||
|
if (splitTunnelType == 0) {
|
||||||
|
tunnelProtocol.excludeLocalNetworks = NO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
m_currentTunnel.protocolConfiguration = tunnelProtocol;
|
m_currentTunnel.protocolConfiguration = tunnelProtocol;
|
||||||
|
|
||||||
|
NETunnelProviderProtocol *appliedProtocol = (NETunnelProviderProtocol *)m_currentTunnel.protocolConfiguration;
|
||||||
|
NSData *ovpnPayload = appliedProtocol.providerConfiguration[@"ovpn"];
|
||||||
|
NSString *payloadPreview = @"";
|
||||||
|
if (ovpnPayload != nil) {
|
||||||
|
NSString *decodedPayload = [[NSString alloc] initWithData:ovpnPayload encoding:NSUTF8StringEncoding];
|
||||||
|
if (decodedPayload != nil) {
|
||||||
|
payloadPreview = [decodedPayload substringToIndex:MIN((NSUInteger)512, decodedPayload.length)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug().noquote() << "IosController::startOpenVPN protocolConfiguration"
|
||||||
|
<< "bundleId=" << QString::fromNSString(appliedProtocol.providerBundleIdentifier ?: @"")
|
||||||
|
<< "serverAddress=" << QString::fromNSString(appliedProtocol.serverAddress ?: @"")
|
||||||
|
<< "providerKeys=" << QString::fromNSString([[appliedProtocol.providerConfiguration.allKeys description] copy])
|
||||||
|
<< "ovpnBytes=" << (ovpnPayload != nil ? ovpnPayload.length : 0);
|
||||||
|
qDebug().noquote() << "IosController::startOpenVPN protocolConfiguration payloadPreview="
|
||||||
|
<< QString::fromNSString(payloadPreview);
|
||||||
|
|
||||||
startTunnel();
|
startTunnel();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -701,7 +869,9 @@ bool IosController::startWireGuard(const QString &config)
|
|||||||
|
|
||||||
NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init];
|
NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init];
|
||||||
tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID];
|
tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID];
|
||||||
tunnelProtocol.providerConfiguration = @{@"wireguard": [[NSString stringWithUTF8String:config.toStdString().c_str()] dataUsingEncoding:NSUTF8StringEncoding]};
|
QByteArray configUtf8 = config.toUtf8();
|
||||||
|
NSData *wgConfigData = [NSData dataWithBytes:configUtf8.constData() length:configUtf8.size()];
|
||||||
|
tunnelProtocol.providerConfiguration = @{@"wireguard": wgConfigData};
|
||||||
tunnelProtocol.serverAddress = m_serverAddress;
|
tunnelProtocol.serverAddress = m_serverAddress;
|
||||||
|
|
||||||
m_currentTunnel.protocolConfiguration = tunnelProtocol;
|
m_currentTunnel.protocolConfiguration = tunnelProtocol;
|
||||||
@@ -715,7 +885,9 @@ bool IosController::startXray(const QString &config)
|
|||||||
|
|
||||||
NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init];
|
NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init];
|
||||||
tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID];
|
tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID];
|
||||||
tunnelProtocol.providerConfiguration = @{@"xray": [[NSString stringWithUTF8String:config.toStdString().c_str()] dataUsingEncoding:NSUTF8StringEncoding]};
|
QByteArray configUtf8 = config.toUtf8();
|
||||||
|
NSData *xrayConfigData = [NSData dataWithBytes:configUtf8.constData() length:configUtf8.size()];
|
||||||
|
tunnelProtocol.providerConfiguration = @{@"xray": xrayConfigData};
|
||||||
tunnelProtocol.serverAddress = m_serverAddress;
|
tunnelProtocol.serverAddress = m_serverAddress;
|
||||||
|
|
||||||
m_currentTunnel.protocolConfiguration = tunnelProtocol;
|
m_currentTunnel.protocolConfiguration = tunnelProtocol;
|
||||||
@@ -737,39 +909,49 @@ void IosController::startTunnel()
|
|||||||
m_rxBytes = 0;
|
m_rxBytes = 0;
|
||||||
m_txBytes = 0;
|
m_txBytes = 0;
|
||||||
|
|
||||||
[m_currentTunnel setEnabled:YES];
|
NETunnelProviderManager *tunnel = m_currentTunnel;
|
||||||
|
[tunnel setEnabled:YES];
|
||||||
|
|
||||||
[m_currentTunnel saveToPreferencesWithCompletionHandler:^(NSError *saveError) {
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
[tunnel saveToPreferencesWithCompletionHandler:^(NSError *saveError) {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
if (saveError) {
|
||||||
|
qDebug().nospace() << "IosController::startTunnel" << protocolName << ": Connect " << protocolName
|
||||||
|
<< " Tunnel Save Error" << saveError.localizedDescription.UTF8String << " domain:"
|
||||||
|
<< saveError.domain.UTF8String << " code:" << saveError.code;
|
||||||
|
emit connectionStateChanged(Vpn::ConnectionState::Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (saveError) {
|
[tunnel loadFromPreferencesWithCompletionHandler:^(NSError *loadError) {
|
||||||
qDebug().nospace() << "IosController::startTunnel" << protocolName << ": Connect " << protocolName << " Tunnel Save Error" << saveError.localizedDescription.UTF8String;
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
emit connectionStateChanged(Vpn::ConnectionState::Error);
|
if (loadError) {
|
||||||
return;
|
qDebug().nospace() << "IosController::startTunnel :" << tunnel.localizedDescription << protocolName
|
||||||
}
|
<< ": Connect " << protocolName << " Tunnel Load Error"
|
||||||
|
<< loadError.localizedDescription.UTF8String;
|
||||||
|
emit connectionStateChanged(Vpn::ConnectionState::Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
[m_currentTunnel loadFromPreferencesWithCompletionHandler:^(NSError *loadError) {
|
NSError *startError = nil;
|
||||||
if (loadError) {
|
qDebug() << iosStatusToState(tunnel.connection.status);
|
||||||
qDebug().nospace() << "IosController::startTunnel :" << m_currentTunnel.localizedDescription << protocolName << ": Connect " << protocolName << " Tunnel Load Error" << loadError.localizedDescription.UTF8String;
|
|
||||||
emit connectionStateChanged(Vpn::ConnectionState::Error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSError *startError = nil;
|
BOOL started = [tunnel.connection startVPNTunnelWithOptions:nil andReturnError:&startError];
|
||||||
qDebug() << iosStatusToState(m_currentTunnel.connection.status);
|
|
||||||
|
|
||||||
BOOL started = [m_currentTunnel.connection startVPNTunnelWithOptions:nil andReturnError:&startError];
|
if (!started || startError) {
|
||||||
|
qDebug().nospace() << "IosController::startTunnel :" << tunnel.localizedDescription << protocolName
|
||||||
if (!started || startError) {
|
<< " : Connect " << protocolName << " Tunnel Start Error"
|
||||||
qDebug().nospace() << "IosController::startTunnel :" << m_currentTunnel.localizedDescription << protocolName << " : Connect " << protocolName << " Tunnel Start Error"
|
<< (startError ? startError.localizedDescription.UTF8String : "");
|
||||||
<< (startError ? startError.localizedDescription.UTF8String : "");
|
emit connectionStateChanged(Vpn::ConnectionState::Error);
|
||||||
emit connectionStateChanged(Vpn::ConnectionState::Error);
|
} else {
|
||||||
} else {
|
qDebug().nospace() << "IosController::startTunnel :" << tunnel.localizedDescription << protocolName
|
||||||
qDebug().nospace() << "IosController::startTunnel :" << m_currentTunnel.localizedDescription << protocolName << " : Starting the tunnel succeeded";
|
<< " : Starting the tunnel succeeded";
|
||||||
}
|
}
|
||||||
}];
|
});
|
||||||
});
|
}];
|
||||||
}];
|
});
|
||||||
|
}];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bool IosController::isOurManager(NETunnelProviderManager* manager) {
|
bool IosController::isOurManager(NETunnelProviderManager* manager) {
|
||||||
@@ -799,6 +981,9 @@ void IosController::sendVpnExtensionMessage(NSDictionary* message, std::function
|
|||||||
{
|
{
|
||||||
if (!m_currentTunnel) {
|
if (!m_currentTunnel) {
|
||||||
qDebug() << "Cannot set an extension callback without a tunnel manager";
|
qDebug() << "Cannot set an extension callback without a tunnel manager";
|
||||||
|
if (callback) {
|
||||||
|
callback(nil);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -808,6 +993,9 @@ void IosController::sendVpnExtensionMessage(NSDictionary* message, std::function
|
|||||||
if (!data || error) {
|
if (!data || error) {
|
||||||
qDebug() << "Failed to serialize message to VpnExtension as JSON. Error:"
|
qDebug() << "Failed to serialize message to VpnExtension as JSON. Error:"
|
||||||
<< [error.localizedDescription UTF8String];
|
<< [error.localizedDescription UTF8String];
|
||||||
|
if (callback) {
|
||||||
|
callback(nil);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -838,11 +1026,18 @@ void IosController::sendVpnExtensionMessage(NSDictionary* message, std::function
|
|||||||
[session sendProviderMessage:data returnError:&sendError responseHandler:completionHandler];
|
[session sendProviderMessage:data returnError:&sendError responseHandler:completionHandler];
|
||||||
} else {
|
} else {
|
||||||
qDebug() << "Method sendProviderMessage:responseHandler:error: does not exist";
|
qDebug() << "Method sendProviderMessage:responseHandler:error: does not exist";
|
||||||
|
if (callback) {
|
||||||
|
callback(nil);
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sendError) {
|
if (sendError) {
|
||||||
qDebug() << "Failed to send message to VpnExtension. Error:"
|
qDebug() << "Failed to send message to VpnExtension. Error:"
|
||||||
<< [sendError.localizedDescription UTF8String];
|
<< [sendError.localizedDescription UTF8String];
|
||||||
|
if (callback) {
|
||||||
|
callback(nil);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -913,6 +1108,147 @@ QString IosController::openFile() {
|
|||||||
return filePath;
|
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 *productInfo in products) {
|
||||||
|
QVariantMap productData;
|
||||||
|
productData["productId"] = QString::fromUtf8([productInfo[@"productId"] UTF8String]);
|
||||||
|
productData["title"] = QString::fromUtf8([productInfo[@"title"] UTF8String]);
|
||||||
|
productData["description"] = QString::fromUtf8([productInfo[@"description"] UTF8String]);
|
||||||
|
productData["price"] = QString::fromUtf8([productInfo[@"price"] UTF8String]);
|
||||||
|
if (productInfo[@"displayPrice"]) {
|
||||||
|
productData["displayPrice"] = QString::fromUtf8([productInfo[@"displayPrice"] UTF8String]);
|
||||||
|
}
|
||||||
|
productData["currencyCode"] = QString::fromUtf8([productInfo[@"currencyCode"] UTF8String]);
|
||||||
|
if (productInfo[@"priceAmount"]) {
|
||||||
|
productData["priceAmount"] = [productInfo[@"priceAmount"] doubleValue];
|
||||||
|
}
|
||||||
|
if (productInfo[@"subscriptionBillingMonths"]) {
|
||||||
|
productData["subscriptionBillingMonths"] = [productInfo[@"subscriptionBillingMonths"] doubleValue];
|
||||||
|
}
|
||||||
|
if (productInfo[@"displayPricePerMonth"]) {
|
||||||
|
productData["displayPricePerMonth"] = QString::fromUtf8([productInfo[@"displayPricePerMonth"] UTF8String]);
|
||||||
|
}
|
||||||
|
outProducts.push_back(productData);
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
void IosController::requestInetAccess() {
|
||||||
NSURL *url = [NSURL URLWithString:@"http://captive.apple.com/generate_204"];
|
NSURL *url = [NSURL URLWithString:@"http://captive.apple.com/generate_204"];
|
||||||
if (!url) {
|
if (!url) {
|
||||||
@@ -931,3 +1267,8 @@ void IosController::requestInetAccess() {
|
|||||||
}];
|
}];
|
||||||
[task resume];
|
[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);
|
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
|
//TODO IMPL FOR AMNEZIA
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||