Compare commits

...

32 Commits

Author SHA1 Message Date
Dan Nguyen 17748cca47 ISSUE: In start page, icon is highlighted not correctly when press ESC key
ROOT CAUSE: The button state is decided by the attribute isServerInfoShow and it was added by commit 68fe20ddf6. The logic to decide whether server info showed is not correct

ACTION: Revert commit 68fe20ddf6
2024-03-04 22:35:34 +07:00
albexk 080e1d98c6 Add Quick Settings tile (#660)
* Add Quick Settings tile

- Add multi-client support to AmneziaVpnService
- Make AmneziaActivity permanently connected to AmneziaVpnService while it is running
- Refactor processing of connection state changes on qt side
- Add VpnState DataStore
- Add check if AmneziaVpnService is running

* Add tile reset when the server is removed from the application
2024-03-04 15:08:55 +00:00
isamnezia ca633ae882 Remove VPN configurations after app reset on iOS (#661) 2024-03-04 12:25:49 +00:00
isamnezia bb7b64fb96 Fix QML glitches and crash on iOS (#658)
Fix QML glitches and crash on iOS and Android
2024-03-03 23:28:10 +00:00
AlexanderGalkov bf901631bf Update Dockerfile (#648)
* Update Dockerfile
* update server scripts for cloak and shadowsocks
* specify the latest cloak and shadowsocks releases in server scripts
2024-03-02 19:45:42 +00:00
Shehab Ahmed 0c0ce54b1f fixed the first-generated QR code is visible while generating another QR code bug (#656) 2024-03-02 19:06:33 +00:00
pokamest ee762c4cef Merge pull request #653 from amnezia-vpn/fix/config-from-tg
Fix adding config from bot (iOS)
2024-02-29 07:05:24 -08:00
pokamest ed9efb5a79 Merge pull request #654 from amnezia-vpn/fix/config-sync-ios
Sync configs and fix bug in NE (iOS)
2024-02-29 03:09:20 -08:00
Igor Sorokin 73eb85f2f4 Sync configs 2024-02-29 13:58:11 +03:00
Nethius cd055cff62 removed the display of servers without containers on PageShare (#609)
* removed the display of servers without containers on PageShare

* removed unused isAnyContainerInstalled() from containers model

* added tab navigation to the share connection drawer

* fixed display of default server without containers on PageShare
2024-02-29 10:22:17 +00:00
Igor Sorokin f8b2cce618 Fix adding config from bot 2024-02-29 08:36:56 +03:00
sa6ta6ni6c e648054c7a Misc update README (#652)
Update README.md
2024-02-29 00:14:09 +00:00
pokamest fe558163cc Merge pull request #651 from amnezia-vpn/bugfix/on-escape-pressed
fixed initial value of m_drawerDepth
2024-02-28 06:35:38 -08:00
vladimir.kuznetsov 3883b8ff34 fixed initial value of m_drawerDepth 2024-02-28 17:31:46 +03:00
pokamest d286664763 Merge pull request #649 from sa6ta6ni6c/patch-1
Ru translation update
2024-02-28 05:31:14 -08:00
Nethius b05ad2392b added escape key handler (#461)
Added escape key handler for drawer2type
2024-02-28 12:39:28 +00:00
Nethius 6dbdb85aaf fixed "file does not exist" error when opening a file for saving (#636) 2024-02-28 12:32:25 +00:00
sa6ta6ni6c 26b48cfe4f Обновил перевод 2024-02-27 20:47:41 +00:00
Andrey Zaharow 2f39136143 Fix translations (#646)
Fix awg texts
2024-02-26 21:17:57 +00:00
pokamest 8d0d3c5ce9 Merge pull request #641 from amnezia-vpn/feature/linux-ipv6
Remove ipv6 address for Linux WG/AWG interface
2024-02-26 12:07:50 -08:00
lunardunno 256081e4ed Improved server cleaning (#639)
Deleting the amnezia directory in opt when cleaning the server.
2024-02-26 12:35:31 +00:00
pokamest 1dd7b0a221 Merge pull request #647 from amnezia-vpn/bugfix/ru-translations
fixed ru translations file
2024-02-26 04:17:42 -08:00
vladimir.kuznetsov 82c0b28906 fixed ru translations file 2024-02-26 17:12:46 +05:00
KsZnak 985fe083f0 split-tunneling translate (#640)
Update amneziavpn_ru.ts
2024-02-26 11:53:22 +00:00
pokamest 6a0000dc4b Merge pull request #642 from amnezia-vpn/translations/update_zh_CN
Update amneziavpn_zh_CN.ts
2024-02-26 03:47:31 -08:00
pokamest 1dd2f38066 Merge pull request #643 from amnezia-vpn/translations/update_fa_IR.ts
Update amneziavpn_fa_IR.ts
2024-02-26 03:45:04 -08:00
pokamest 004e1e3ca5 MacOS GH actions QIF fix (#645)
Install Qt Installer Framework 4.6 from R2 to keep compatibility for old MacOS. In addition, update Qt version in build scripts.
2024-02-26 10:44:28 +00:00
KsZnak 7c560d709b Update amneziavpn_fa_IR.ts 2024-02-24 22:16:18 +02:00
KsZnak d3743ad62f Update amneziavpn_zh_CN.ts 2024-02-24 22:06:05 +02:00
pokamest ac234b77e2 Merge pull request #638 from amnezia-vpn/update/update_ts
Update all translations
2024-02-24 06:59:50 -08:00
Mykola Baibuz 9886987e68 Remove ipv6 address for Linux WG/AWG interface 2024-02-24 16:07:59 +02:00
pokamest d34cb8898f Update all translations 2024-02-24 11:51:22 +00:00
69 changed files with 2701 additions and 1948 deletions
+7 -2
View File
@@ -227,7 +227,7 @@ jobs:
env: env:
# Keep compat with MacOS 10.15 aka Catalina by Qt 6.4 # Keep compat with MacOS 10.15 aka Catalina by Qt 6.4
QT_VERSION: 6.4.3 QT_VERSION: 6.4.3
QIF_VERSION: 4.7 QIF_VERSION: 4.6
steps: steps:
- name: 'Setup xcode' - name: 'Setup xcode'
@@ -245,10 +245,15 @@ jobs:
modules: 'qtremoteobjects qt5compat qtshadertools' modules: 'qtremoteobjects qt5compat qtshadertools'
dir: ${{ runner.temp }} dir: ${{ runner.temp }}
setup-python: 'true' setup-python: 'true'
tools: 'tools_ifw'
set-env: 'true' set-env: 'true'
extra: '--external 7z --base ${{ env.QT_MIRROR }}' extra: '--external 7z --base ${{ env.QT_MIRROR }}'
- name: 'Install Qt Installer Framework ${{ env.QIF_VERSION }}'
run: |
mkdir -pv ${{ runner.temp }}/Qt/Tools/QtInstallerFramework
wget https://qt.amzsvc.com/tools/ifw/${{ env.QIF_VERSION }}.zip
unzip ${{ env.QIF_VERSION }}.zip -d ${{ runner.temp }}/Qt/Tools/QtInstallerFramework/
- name: 'Get sources' - name: 'Get sources'
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
+1 -1
View File
@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
set(PROJECT AmneziaVPN) set(PROJECT AmneziaVPN)
project(${PROJECT} VERSION 4.4.0.0 project(${PROJECT} VERSION 4.4.1.2
DESCRIPTION "AmneziaVPN" DESCRIPTION "AmneziaVPN"
HOMEPAGE_URL "https://amnezia.org/" HOMEPAGE_URL "https://amnezia.org/"
) )
+39 -35
View File
@@ -7,13 +7,15 @@
Amnezia is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server. Amnezia is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server.
## Features ## Features
- Very easy to use - enter your ip address, ssh login and password, and Amnezia will automatically install VPN docker containers to your server and connect to VPN.
- OpenVPN, ShadowSocks, WireGuard, IKEv2 protocols support. - Very easy to use - enter your IP address, SSH login, and password, and Amnezia will automatically install VPN docker containers to your server and connect to the VPN.
- OpenVPN, ShadowSocks, WireGuard, and IKEv2 protocols support.
- Masking VPN with OpenVPN over Cloak plugin - Masking VPN with OpenVPN over Cloak plugin
- Split tunneling support - add any sites to client to enable VPN only for them (only for desktops) - Split tunneling support - add any sites to the client to enable VPN only for them (only for desktops)
- Windows, MacOS, Linux, Android, iOS releases. - Windows, MacOS, Linux, Android, iOS releases.
## Links ## Links
[https://amnezia.org](https://amnezia.org) - project website [https://amnezia.org](https://amnezia.org) - project website
[https://www.reddit.com/r/AmneziaVPN](https://www.reddit.com/r/AmneziaVPN) - Reddit [https://www.reddit.com/r/AmneziaVPN](https://www.reddit.com/r/AmneziaVPN) - Reddit
[https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Telegram support channel (English) [https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Telegram support channel (English)
@@ -21,13 +23,13 @@ Amnezia is an open-source VPN client, with a key feature that enables you to dep
## Tech ## Tech
AmneziaVPN uses a number of open source projects to work: AmneziaVPN uses several open-source projects to work:
- [OpenSSL](https://www.openssl.org/) - [OpenSSL](https://www.openssl.org/)
- [OpenVPN](https://openvpn.net/) - [OpenVPN](https://openvpn.net/)
- [ShadowSocks](https://shadowsocks.org/) - [ShadowSocks](https://shadowsocks.org/)
- [Qt](https://www.qt.io/) - [Qt](https://www.qt.io/)
- [LibSsh](https://libssh.org) - forked form Qt Creator - [LibSsh](https://libssh.org) - forked from Qt Creator
- and more... - and more...
## Checking out the source code ## Checking out the source code
@@ -43,14 +45,15 @@ git submodule update --init --recursive
Want to contribute? Welcome! Want to contribute? Welcome!
### Building sources and deployment ### Building sources and deployment
Look deploy folder for build scripts.
### How to build iOS app from source code on MacOS Check deploy folder for build scripts.
### How to build an iOS app from source code on MacOS
1. First, make sure you have [XCode](https://developer.apple.com/xcode/) installed, at least version 14 or higher. 1. First, make sure you have [XCode](https://developer.apple.com/xcode/) installed, at least version 14 or higher.
2. We use QT to generate the XCode project. we need QT version 6.6.1. Install QT for macos in [here](https://doc.qt.io/qt-6/macos.html) or [QT Online Installer](https://www.qt.io/download-open-source). Required modules: 2. We use QT to generate the XCode project. We need QT version 6.6.1. Install QT for MacOS [here](https://doc.qt.io/qt-6/macos.html) or [QT Online Installer](https://www.qt.io/download-open-source). Required modules:
- macOS - MacOS
- iOS - iOS
- Qt 5 Compatibility Module - Qt 5 Compatibility Module
- Qt Shader Tools - Qt Shader Tools
@@ -59,18 +62,18 @@ Look deploy folder for build scripts.
- Qt Multimedia - Qt Multimedia
- Qt Remote Objects - Qt Remote Objects
3. Install cmake is require. We recommend cmake version 3.25. You can install cmake in [here](https://cmake.org/download/) 3. Install CMake if required. We recommend CMake version 3.25. You can install CMake [here](https://cmake.org/download/)
4. You also need to install go >= v1.16. If you don't have it done already, 4. You also need to install go >= v1.16. If you don't have it installed already,
download go from the [official website](https://golang.org/dl/) or use Homebrew. download go from the [official website](https://golang.org/dl/) or use Homebrew.
Latest version is recommended. Install gomobile The latest version is recommended. Install gomobile
```bash ```bash
export PATH=$PATH:~/go/bin export PATH=$PATH:~/go/bin
go install golang.org/x/mobile/cmd/gomobile@latest go install golang.org/x/mobile/cmd/gomobile@latest
gomobile init gomobile init
``` ```
5. Build project 5. Build the project
```bash ```bash
export QT_BIN_DIR="<PATH-TO-QT-FOLDER>/Qt/<QT-VERSION>/ios/bin" export QT_BIN_DIR="<PATH-TO-QT-FOLDER>/Qt/<QT-VERSION>/ios/bin"
export QT_MACOS_ROOT_DIR="<PATH-TO-QT-FOLDER>/Qt/<QT-VERSION>/macos" export QT_MACOS_ROOT_DIR="<PATH-TO-QT-FOLDER>/Qt/<QT-VERSION>/macos"
@@ -88,62 +91,63 @@ of the bin folder where gomobile was installed. Usually, it's in `GOPATH`.
export PATH=$(PATH):/path/to/GOPATH/bin export PATH=$(PATH):/path/to/GOPATH/bin
``` ```
5. Open XCode project. You can then run/test/archive/ship the app. 6. Open the XCode project. You can then run /test/archive/ship the app.
If build fails with the following error If the build fails with the following error
``` ```
make: *** make: ***
[$(PROJECTDIR)/client/build/AmneziaVPN.build/Debug-iphoneos/wireguard-go-bridge/goroot/.prepared] [$(PROJECTDIR)/client/build/AmneziaVPN.build/Debug-iphoneos/wireguard-go-bridge/goroot/.prepared]
Error 1 Error 1
``` ```
Add a user defined variable to both AmneziaVPN and WireGuardNetworkExtension targets' build settings with Add a user-defined variable to both AmneziaVPN and WireGuardNetworkExtension targets' build settings with
key `PATH` and value `${PATH}/path/to/bin/folder/with/go/executable`, e.g. `${PATH}:/usr/local/go/bin`. key `PATH` and value `${PATH}/path/to/bin/folder/with/go/executable`, e.g. `${PATH}:/usr/local/go/bin`.
if above error still persists on you M1 Mac, then most probably you need to install arch based cmake if the above error persists on your M1 Mac, then most probably you need to install arch based CMake
``` ```
arch -arm64 brew install cmake arch -arm64 brew install cmake
``` ```
Build might fail with "source files not found" error the first time you try it, because modern XCode build system compiles Build might fail with the "source files not found" error the first time you try it, because the modern XCode build system compiles dependencies in parallel, and some dependencies end up being built after the ones that
dependencies in parallel, and some dependencies end up being built after the ones that require them. In this case, simply restart the build.
require them. In this case simply restart the build.
## How to build the Android app ## How to build the Android app
_tested on Mac OS_
_Tested on Mac OS_
The Android app has the following requirements: The Android app has the following requirements:
* JDK 11 * JDK 11
* Android platform SDK 33 * Android platform SDK 33
* cmake 3.25.0 * CMake 3.25.0
After you have installed QT, QT Creator and Android Studio installed, you need to configure QT Creator correctly. Click in the top menu bar on `QT Creator` -> `Preferences` -> `Devices` and select the tab `Android`. After you have installed QT, QT Creator, and Android Studio, you need to configure QT Creator correctly. Click in the top menu bar on `QT Creator` -> `Preferences` -> `Devices` and select the tab `Android`.
* set path to jdk 11 * set path to JDK 11
* set path to Android SDK ($ANDROID_HOME) * set path to Android SDK ($ANDROID_HOME)
In case you get errors regarding missing SDK or 'sdkmanager not running', you cannot fix them by correcting the paths and you have some spare GBs on your disk, you can let QT Creator install all requirements by choosing an empty folder for `Android SDK location` and click on `Set Up SDK`. Be aware: This will install a second Android SDK and NDK on your machine! In case you get errors regarding missing SDK or 'SDK manager not running', you cannot fix them by correcting the paths. If you have some spare GBs on your disk, you can let QT Creator install all requirements by choosing an empty folder for `Android SDK location` and clicking on `Set Up SDK`. Be aware: This will install a second Android SDK and NDK on your machine! 
Double-check that the right CMake version is configured:  Click on `QT Creator` -> `Preferences` and click on the side menu on `Kits`. Under the center content view's `Kits` tab, you'll find an entry for `CMake Tool`. If the default selected CMake version is lower than 3.25.0, install on your system CMake >= 3.25.0 and choose `System CMake at <path>` from the drop-down list. If this entry is missing, you either have not installed CMake yet or QT Creator hasn't found the path to it. In that case, click in the preferences window on the side menu item `CMake`, then on the tab `Tools` in the center content view, and finally on the button `Add` to set the path to your installed CMake. 
Please make sure that you have selected Android Platform SDK 33 for your project: click in the main view's side menu on `Projects`, and on the left, you'll see a section `Build & Run` showing different Android build targets. You can select any of them, Amnezia VPN's project setup is designed in a way that all Android targets will be built. Click on the targets submenu item `Build` and scroll in the center content view to `Build Steps`. Click on `Details` at the end of the headline `Build Android APK` (the `Details` button might be hidden in case the QT Creator Window is not running in full screen!). Here we are: Choose `android-33` as `Android Build Platform SDK`.
Double check that the right cmake version is configured: Click on `QT Creator` -> `Preferences` and click on the side menu on `Kits`. Under the center content view's `Kits` tab you'll find an entry `CMake Tool`. If the default selected CMake version is lower than 3.25.0, install on your system CMake >= 3.25.0 and choose `System CMake at <path>` from the drop down list. If this entry is missing, you either have not installed CMake yet or QT Creator hasn't found the path to it. In that case click in the preferences window on the side menu item `CMake`, then on the tab `Tools`in the center content view and finally on the Button `Add` to set the path to your installed CMake. That's it! You should be ready to compile the project from QT Creator!
Please make sure that you have selected Android Platform SDK 33 for your project: click in the main view's side menu on on `Projects`, on the left you'll see a section `Build & Run` showing different Android build targets. You can select any of them, Amnezia VPN's project setup is designed in a way that always all Android targets will be build. Click on the targets submenu item `Build` and scroll in the center content view to `Build Steps`. Click on `Details` at the end of the headline `Build Android APK` (The `Details` button might be hidden in case QT Creator Window is not running in full screen!). Here we are: choose `android-33` as `Android Build platform SDK`.
That's it you should be ready to compile the project from QT Creator!
### Development flow ### Development flow
After you've hit the build button, QT-Creator copies the whole project to a folder in the repositories parent directory. The folder should look something like `build-amnezia-client-Android_Qt_<version>_Clang_<architecture>-<BuildType>`.
If you want to develop Amnezia VPNs Android components written in Kotlin, such as components using system APIs, you need to import the generated project in Android Studio with `build-amnezia-client-Android_Qt_<version>_Clang_<architecture>-<BuildType>/client/android-build` as the projects root directory. While you should be able to compile the generated project from Android Studio, you cannot work directly in the repository's Android project. So whenever you are confident with your work in the generated project, you'll need to copy and paste the affected files to the corresponding path in the repositories Android project so that you can add and commit your changes!
You may face compiling issues in QT Creator after you've worked in Android Studio on the generated project. Just do a `./gradlew clean` in the generated project's root directory (`<path>/client/android-build/.`) and you should be good to continue. After you've hit the build button, QT-Creator copies the whole project to a folder in the repository parent directory. The folder should look something like `build-amnezia-client-Android_Qt_<version>_Clang_<architecture>-<BuildType>`.
If you want to develop Amnezia VPNs Android components written in Kotlin, such as components using system APIs, you need to import the generated project in Android Studio with `build-amnezia-client-Android_Qt_<version>_Clang_<architecture>-<BuildType>/client/android-build` as the projects root directory. While you should be able to compile the generated project from Android Studio, you cannot work directly in the repository's Android project. So whenever you are confident with your work in the generated project, you'll need to copy and paste the affected files to the corresponding path in the repository's Android project so that you can add and commit your changes!
You may face compiling issues in QT Creator after you've worked in Android Studio on the generated project. Just do a `./gradlew clean` in the generated project's root directory (`<path>/client/android-build/.`) and you should be good to go.
## License ## License
GPL v.3
GPL v3.0
## Donate ## Donate
Bitcoin: bc1qn9rhsffuxwnhcuuu4qzrwp4upkrq94xnh8r26u Bitcoin: bc1qn9rhsffuxwnhcuuu4qzrwp4upkrq94xnh8r26u
XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3 XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3
payeer.com: P2561305 payeer.com: P2561305
ko-fi.com: [https://ko-fi.com/amnezia_vpn](https://ko-fi.com/amnezia_vpn) ko-fi.com: [https://ko-fi.com/amnezia_vpn](https://ko-fi.com/amnezia_vpn)
## Acknowledgments
## etc
This project is tested with BrowserStack. This project is tested with BrowserStack.
We express our gratitude to [BrowserStack](https://www.browserstack.com) for supporting our project. We express our gratitude to [BrowserStack](https://www.browserstack.com) for supporting our project.
+8 -1
View File
@@ -95,7 +95,14 @@ void AmneziaApplication::init()
qFatal("Android logging initialization failed"); qFatal("Android logging initialization failed");
} }
AndroidController::instance()->setSaveLogs(m_settings->isSaveLogs()); AndroidController::instance()->setSaveLogs(m_settings->isSaveLogs());
connect(m_settings.get(), &Settings::saveLogsChanged, AndroidController::instance(), &AndroidController::setSaveLogs); connect(m_settings.get(), &Settings::saveLogsChanged,
AndroidController::instance(), &AndroidController::setSaveLogs);
connect(m_settings.get(), &Settings::serverRemoved,
AndroidController::instance(), &AndroidController::resetLastServer);
connect(m_settings.get(), &Settings::settingsCleared,
[](){ AndroidController::instance()->resetLastServer(-1); });
connect(AndroidController::instance(), &AndroidController::initConnectionState, this, connect(AndroidController::instance(), &AndroidController::initConnectionState, this,
[this](Vpn::ConnectionState state) { [this](Vpn::ConnectionState state) {
+20
View File
@@ -56,6 +56,10 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</intent-filter>
<meta-data <meta-data
android:name="android.app.lib_name" android:name="android.app.lib_name"
android:value="-- %%INSERT_APP_LIB_NAME%% --" /> android:value="-- %%INSERT_APP_LIB_NAME%% --" />
@@ -146,6 +150,22 @@
</intent-filter> </intent-filter>
</service> </service>
<service
android:name=".AmneziaTileService"
android:process=":amneziaTileService"
android:icon="@drawable/ic_amnezia_round"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
android:exported="true">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
<meta-data
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
android:value="true" />
</service>
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="org.amnezia.vpn.qtprovider" android:authorities="org.amnezia.vpn.qtprovider"
+1
View File
@@ -111,4 +111,5 @@ dependencies {
implementation(libs.kotlinx.coroutines) implementation(libs.kotlinx.coroutines)
implementation(libs.bundles.androidx.camera) implementation(libs.bundles.androidx.camera)
implementation(libs.google.mlkit) implementation(libs.google.mlkit)
implementation(libs.androidx.datastore)
} }
+2
View File
@@ -6,6 +6,7 @@ androidx-activity = "1.8.1"
androidx-annotation = "1.7.0" androidx-annotation = "1.7.0"
androidx-camera = "1.3.0" androidx-camera = "1.3.0"
androidx-security-crypto = "1.1.0-alpha06" androidx-security-crypto = "1.1.0-alpha06"
androidx-datastore = "1.1.0-beta01"
kotlinx-coroutines = "1.7.3" kotlinx-coroutines = "1.7.3"
google-mlkit = "17.2.0" google-mlkit = "17.2.0"
@@ -18,6 +19,7 @@ androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.r
androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "androidx-camera" } androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "androidx-camera" }
androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "androidx-camera" } androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "androidx-camera" }
androidx-security-crypto = { module = "androidx.security:security-crypto-ktx", version.ref = "androidx-security-crypto" } androidx-security-crypto = { module = "androidx.security:security-crypto-ktx", version.ref = "androidx-security-crypto" }
androidx-datastore = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-datastore" }
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" } kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
google-mlkit = { module = "com.google.mlkit:barcode-scanning", version.ref = "google-mlkit" } google-mlkit = { module = "com.google.mlkit:barcode-scanning", version.ref = "google-mlkit" }
@@ -2,9 +2,9 @@ package org.amnezia.vpn.protocol
// keep synchronized with client/platforms/android/android_controller.h ConnectionState // keep synchronized with client/platforms/android/android_controller.h ConnectionState
enum class ProtocolState { enum class ProtocolState {
DISCONNECTED,
CONNECTED, CONNECTED,
CONNECTING, CONNECTING,
DISCONNECTED,
DISCONNECTING, DISCONNECTING,
RECONNECTING, RECONNECTING,
UNKNOWN UNKNOWN
@@ -28,6 +28,10 @@ fun Bundle.putStatus(status: Status) {
putInt(STATE_KEY, status.state.ordinal) putInt(STATE_KEY, status.state.ordinal)
} }
fun Bundle.putStatus(state: ProtocolState) {
putInt(STATE_KEY, state.ordinal)
}
fun Bundle.getStatus(): Status = fun Bundle.getStatus(): Status =
Status.build { Status.build {
setState(ProtocolState.entries[getInt(STATE_KEY)]) setState(ProtocolState.entries[getInt(STATE_KEY)])
+5
View File
@@ -0,0 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="connecting">Подключение</string>
<string name="disconnecting">Отключение</string>
</resources>
+5
View File
@@ -0,0 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="connecting">Connecting</string>
<string name="disconnecting">Disconnecting</string>
</resources>
@@ -26,9 +26,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.amnezia.vpn.protocol.ProtocolState
import org.amnezia.vpn.protocol.getStatistics import org.amnezia.vpn.protocol.getStatistics
import org.amnezia.vpn.protocol.getStatus import org.amnezia.vpn.protocol.getStatus
import org.amnezia.vpn.qt.QtAndroidController import org.amnezia.vpn.qt.QtAndroidController
@@ -36,11 +34,11 @@ import org.amnezia.vpn.util.Log
import org.qtproject.qt.android.bindings.QtActivity import org.qtproject.qt.android.bindings.QtActivity
private const val TAG = "AmneziaActivity" private const val TAG = "AmneziaActivity"
const val ACTIVITY_MESSENGER_NAME = "Activity"
private const val CHECK_VPN_PERMISSION_ACTION_CODE = 1 private const val CHECK_VPN_PERMISSION_ACTION_CODE = 1
private const val CREATE_FILE_ACTION_CODE = 2 private const val CREATE_FILE_ACTION_CODE = 2
private const val OPEN_FILE_ACTION_CODE = 3 private const val OPEN_FILE_ACTION_CODE = 3
private const val BIND_SERVICE_TIMEOUT = 1000L
class AmneziaActivity : QtActivity() { class AmneziaActivity : QtActivity() {
@@ -58,25 +56,17 @@ class AmneziaActivity : QtActivity() {
val event = msg.extractIpcMessage<ServiceEvent>() val event = msg.extractIpcMessage<ServiceEvent>()
Log.d(TAG, "Handle event: $event") Log.d(TAG, "Handle event: $event")
when (event) { when (event) {
ServiceEvent.CONNECTED -> { ServiceEvent.STATUS_CHANGED -> {
QtAndroidController.onVpnConnected() msg.data?.getStatus()?.let { (state) ->
} Log.d(TAG, "Handle protocol state: $state")
QtAndroidController.onVpnStateChanged(state.ordinal)
ServiceEvent.DISCONNECTED -> { }
QtAndroidController.onVpnDisconnected()
doUnbindService()
}
ServiceEvent.RECONNECTING -> {
QtAndroidController.onVpnReconnecting()
} }
ServiceEvent.STATUS -> { ServiceEvent.STATUS -> {
if (isWaitingStatus) { if (isWaitingStatus) {
isWaitingStatus = false isWaitingStatus = false
msg.data?.getStatus()?.let { (state) -> msg.data?.getStatus()?.let { QtAndroidController.onStatus(it) }
QtAndroidController.onStatus(state.ordinal)
}
} }
} }
@@ -87,7 +77,7 @@ class AmneziaActivity : QtActivity() {
} }
ServiceEvent.ERROR -> { ServiceEvent.ERROR -> {
msg.data?.getString(ERROR_MSG)?.let { error -> msg.data?.getString(MSG_ERROR)?.let { error ->
Log.e(TAG, "From VpnService: $error") Log.e(TAG, "From VpnService: $error")
} }
// todo: add error reporting to Qt // todo: add error reporting to Qt
@@ -109,14 +99,15 @@ class AmneziaActivity : QtActivity() {
// get a messenger from the service to send actions to the service // get a messenger from the service to send actions to the service
vpnServiceMessenger.set(Messenger(service)) vpnServiceMessenger.set(Messenger(service))
// send a messenger to the service to process service events // send a messenger to the service to process service events
vpnServiceMessenger.send { vpnServiceMessenger.send(
Action.REGISTER_CLIENT.packToMessage().apply { Action.REGISTER_CLIENT.packToMessage {
replyTo = activityMessenger putString(MSG_CLIENT_NAME, ACTIVITY_MESSENGER_NAME)
} },
} replyTo = activityMessenger
)
isServiceConnected = true isServiceConnected = true
if (isWaitingStatus) { if (isWaitingStatus) {
vpnServiceMessenger.send(Action.REQUEST_STATUS) vpnServiceMessenger.send(Action.REQUEST_STATUS, replyTo = activityMessenger)
} }
} }
@@ -126,6 +117,7 @@ class AmneziaActivity : QtActivity() {
vpnServiceMessenger.reset() vpnServiceMessenger.reset()
isWaitingStatus = true isWaitingStatus = true
QtAndroidController.onServiceDisconnected() QtAndroidController.onServiceDisconnected()
doBindService()
} }
override fun onBindingDied(name: ComponentName?) { override fun onBindingDied(name: ComponentName?) {
@@ -148,8 +140,11 @@ class AmneziaActivity : QtActivity() {
Log.d(TAG, "Create Amnezia activity: $intent") Log.d(TAG, "Create Amnezia activity: $intent")
mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
vpnServiceMessenger = IpcMessenger( vpnServiceMessenger = IpcMessenger(
onDeadObjectException = ::doUnbindService, "VpnService",
messengerName = "VpnService" onDeadObjectException = {
doUnbindService()
doBindService()
}
) )
intent?.let(::processIntent) intent?.let(::processIntent)
} }
@@ -244,10 +239,9 @@ class AmneziaActivity : QtActivity() {
private fun doBindService() { private fun doBindService() {
Log.d(TAG, "Bind service") Log.d(TAG, "Bind service")
Intent(this, AmneziaVpnService::class.java).also { Intent(this, AmneziaVpnService::class.java).also {
bindService(it, serviceConnection, BIND_ABOVE_CLIENT) bindService(it, serviceConnection, BIND_ABOVE_CLIENT and BIND_AUTO_CREATE)
} }
isInBoundState = true isInBoundState = true
handleBindTimeout()
} }
@MainThread @MainThread
@@ -256,26 +250,14 @@ class AmneziaActivity : QtActivity() {
Log.d(TAG, "Unbind service") Log.d(TAG, "Unbind service")
isWaitingStatus = true isWaitingStatus = true
QtAndroidController.onServiceDisconnected() QtAndroidController.onServiceDisconnected()
vpnServiceMessenger.reset()
isServiceConnected = false isServiceConnected = false
vpnServiceMessenger.send(Action.UNREGISTER_CLIENT, activityMessenger)
vpnServiceMessenger.reset()
isInBoundState = false isInBoundState = false
unbindService(serviceConnection) unbindService(serviceConnection)
} }
} }
private fun handleBindTimeout() {
mainScope.launch {
if (isWaitingStatus) {
delay(BIND_SERVICE_TIMEOUT)
if (isWaitingStatus && !isServiceConnected) {
Log.d(TAG, "Bind timeout, reset connection status")
isWaitingStatus = false
QtAndroidController.onStatus(ProtocolState.DISCONNECTED.ordinal)
}
}
}
}
/** /**
* Methods of starting and stopping VpnService * Methods of starting and stopping VpnService
*/ */
@@ -312,7 +294,7 @@ class AmneziaActivity : QtActivity() {
Log.d(TAG, "Connect to VPN") Log.d(TAG, "Connect to VPN")
vpnServiceMessenger.send { vpnServiceMessenger.send {
Action.CONNECT.packToMessage { Action.CONNECT.packToMessage {
putString(VPN_CONFIG, vpnConfig) putString(MSG_VPN_CONFIG, vpnConfig)
} }
} }
} }
@@ -320,7 +302,7 @@ class AmneziaActivity : QtActivity() {
private fun startVpnService(vpnConfig: String) { private fun startVpnService(vpnConfig: String) {
Log.d(TAG, "Start VPN service") Log.d(TAG, "Start VPN service")
Intent(this, AmneziaVpnService::class.java).apply { Intent(this, AmneziaVpnService::class.java).apply {
putExtra(VPN_CONFIG, vpnConfig) putExtra(MSG_VPN_CONFIG, vpnConfig)
}.also { }.also {
ContextCompat.startForegroundService(this, it) ContextCompat.startForegroundService(this, it)
} }
@@ -369,6 +351,22 @@ class AmneziaActivity : QtActivity() {
} }
} }
@Suppress("unused")
fun resetLastServer(index: Int) {
Log.v(TAG, "Reset server: $index")
mainScope.launch {
VpnStateStore.store {
if (index == -1 || it.serverIndex == index) {
VpnState.defaultState
} else if (it.serverIndex > index) {
it.copy(serverIndex = it.serverIndex - 1)
} else {
it
}
}
}
}
@Suppress("unused") @Suppress("unused")
fun saveFile(fileName: String, data: String) { fun saveFile(fileName: String, data: String) {
Log.d(TAG, "Save file $fileName") Log.d(TAG, "Save file $fileName")
@@ -438,7 +436,7 @@ class AmneziaActivity : QtActivity() {
Log.saveLogs = enabled Log.saveLogs = enabled
vpnServiceMessenger.send { vpnServiceMessenger.send {
Action.SET_SAVE_LOGS.packToMessage { Action.SET_SAVE_LOGS.packToMessage {
putBoolean(SAVE_LOGS, enabled) putBoolean(MSG_SAVE_LOGS, enabled)
} }
} }
} }
@@ -18,6 +18,7 @@ class AmneziaApplication : QtApplication(), CameraXConfig.Provider {
super.onCreate() super.onCreate()
Prefs.init(this) Prefs.init(this)
Log.init(this) Log.init(this)
VpnStateStore.init(this)
Log.d(TAG, "Create Amnezia application") Log.d(TAG, "Create Amnezia application")
createNotificationChannel() createNotificationChannel()
} }
@@ -0,0 +1,272 @@
package org.amnezia.vpn
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.net.VpnService
import android.os.Build
import android.os.IBinder
import android.os.Messenger
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import androidx.core.content.ContextCompat
import kotlin.LazyThreadSafetyMode.NONE
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.amnezia.vpn.protocol.ProtocolState
import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
import org.amnezia.vpn.protocol.ProtocolState.CONNECTING
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTING
import org.amnezia.vpn.protocol.ProtocolState.RECONNECTING
import org.amnezia.vpn.protocol.ProtocolState.UNKNOWN
import org.amnezia.vpn.util.Log
private const val TAG = "AmneziaTileService"
private const val DEFAULT_TILE_LABEL = "AmneziaVPN"
class AmneziaTileService : TileService() {
private lateinit var scope: CoroutineScope
private var vpnStateListeningJob: Job? = null
private lateinit var vpnServiceMessenger: IpcMessenger
@Volatile
private var isServiceConnected = false
private var isInBoundState = false
@Volatile
private var isVpnConfigExists = false
private val serviceConnection: ServiceConnection by lazy(NONE) {
object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
Log.d(TAG, "Service ${name?.flattenToString()} was connected")
vpnServiceMessenger.set(Messenger(service))
isServiceConnected = true
}
override fun onServiceDisconnected(name: ComponentName?) {
Log.w(TAG, "Service ${name?.flattenToString()} was unexpectedly disconnected")
isServiceConnected = false
vpnServiceMessenger.reset()
updateVpnState(DISCONNECTED)
}
override fun onBindingDied(name: ComponentName?) {
Log.w(TAG, "Binding to the ${name?.flattenToString()} unexpectedly died")
doUnbindService()
doBindService()
}
}
}
override fun onCreate() {
super.onCreate()
Log.d(TAG, "Create Amnezia Tile Service")
scope = CoroutineScope(SupervisorJob())
vpnServiceMessenger = IpcMessenger(
"VpnService",
onDeadObjectException = ::doUnbindService
)
}
override fun onDestroy() {
Log.d(TAG, "Destroy Amnezia Tile Service")
doUnbindService()
scope.cancel()
super.onDestroy()
}
// Workaround for some bugs
override fun onBind(intent: Intent?): IBinder? =
try {
super.onBind(intent)
} catch (e: Throwable) {
Log.e(TAG, "Failed to bind AmneziaTileService: $e")
null
}
override fun onStartListening() {
super.onStartListening()
Log.d(TAG, "Start listening")
if (AmneziaVpnService.isRunning(applicationContext)) {
Log.d(TAG, "Vpn service is running")
doBindService()
} else {
Log.d(TAG, "Vpn service is not running")
isServiceConnected = false
updateVpnState(DISCONNECTED)
}
vpnStateListeningJob = launchVpnStateListening()
}
override fun onStopListening() {
Log.d(TAG, "Stop listening")
vpnStateListeningJob?.cancel()
vpnStateListeningJob = null
doUnbindService()
super.onStopListening()
}
override fun onClick() {
Log.d(TAG, "onClick")
if (isLocked) {
unlockAndRun { onClickInternal() }
} else {
onClickInternal()
}
}
private fun onClickInternal() {
if (isVpnConfigExists) {
Log.d(TAG, "Change VPN state")
if (qsTile.state == Tile.STATE_INACTIVE) {
Log.d(TAG, "Start VPN")
updateVpnState(CONNECTING)
startVpn()
} else if (qsTile.state == Tile.STATE_ACTIVE) {
Log.d(TAG, "Stop vpn")
updateVpnState(DISCONNECTING)
stopVpn()
}
} else {
Log.d(TAG, "Start Activity")
Intent(this, AmneziaActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}.also {
startActivityAndCollapseCompat(it)
}
}
}
private fun doBindService() {
Log.d(TAG, "Bind service")
Intent(this, AmneziaVpnService::class.java).also {
bindService(it, serviceConnection, BIND_ABOVE_CLIENT)
}
isInBoundState = true
}
private fun doUnbindService() {
if (isInBoundState) {
Log.d(TAG, "Unbind service")
isServiceConnected = false
vpnServiceMessenger.reset()
isInBoundState = false
unbindService(serviceConnection)
}
}
private fun startVpn() {
if (isServiceConnected) {
connectToVpn()
} else {
if (checkPermission()) {
startVpnService()
doBindService()
} else {
updateVpnState(DISCONNECTED)
}
}
}
private fun checkPermission() =
if (VpnService.prepare(applicationContext) != null) {
Intent(this, VpnRequestActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}.also {
startActivityAndCollapseCompat(it)
}
false
} else {
true
}
private fun startVpnService() =
ContextCompat.startForegroundService(
applicationContext,
Intent(this, AmneziaVpnService::class.java)
)
private fun connectToVpn() = vpnServiceMessenger.send(Action.CONNECT)
private fun stopVpn() = vpnServiceMessenger.send(Action.DISCONNECT)
@SuppressLint("StartActivityAndCollapseDeprecated")
private fun startActivityAndCollapseCompat(intent: Intent) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startActivityAndCollapse(
PendingIntent.getActivity(
applicationContext,
0,
intent,
PendingIntent.FLAG_IMMUTABLE
)
)
} else {
@Suppress("DEPRECATION")
startActivityAndCollapse(intent)
}
}
private fun updateVpnState(state: ProtocolState) {
scope.launch {
VpnStateStore.store { it.copy(protocolState = state) }
}
}
private fun launchVpnStateListening() =
scope.launch { VpnStateStore.dataFlow().collectLatest(::updateTile) }
private fun updateTile(vpnState: VpnState) {
Log.d(TAG, "Update tile: $vpnState")
isVpnConfigExists = vpnState.serverName != null
val tile = qsTile ?: return
tile.apply {
label = vpnState.serverName ?: DEFAULT_TILE_LABEL
when (vpnState.protocolState) {
CONNECTED -> {
state = Tile.STATE_ACTIVE
subtitleCompat = null
}
DISCONNECTED, UNKNOWN -> {
state = Tile.STATE_INACTIVE
subtitleCompat = null
}
CONNECTING, RECONNECTING -> {
state = Tile.STATE_UNAVAILABLE
subtitleCompat = resources.getString(R.string.connecting)
}
DISCONNECTING -> {
state = Tile.STATE_UNAVAILABLE
subtitleCompat = resources.getString(R.string.disconnecting)
}
}
updateTile()
}
// double update to fix weird visual glitches
tile.updateTile()
}
private var Tile.subtitleCompat: CharSequence?
set(value) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
this.subtitle = value
}
}
get() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
return this.subtitle
}
return null
}
}
@@ -1,7 +1,10 @@
package org.amnezia.vpn package org.amnezia.vpn
import android.app.ActivityManager
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE
import android.app.Notification import android.app.Notification
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED
@@ -16,6 +19,7 @@ import android.os.Process
import androidx.annotation.MainThread import androidx.annotation.MainThread
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
import java.util.concurrent.ConcurrentHashMap
import kotlin.LazyThreadSafetyMode.NONE import kotlin.LazyThreadSafetyMode.NONE
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -26,6 +30,7 @@ import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@@ -39,14 +44,11 @@ import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTING import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTING
import org.amnezia.vpn.protocol.ProtocolState.RECONNECTING import org.amnezia.vpn.protocol.ProtocolState.RECONNECTING
import org.amnezia.vpn.protocol.ProtocolState.UNKNOWN import org.amnezia.vpn.protocol.ProtocolState.UNKNOWN
import org.amnezia.vpn.protocol.Statistics
import org.amnezia.vpn.protocol.Status
import org.amnezia.vpn.protocol.VpnException import org.amnezia.vpn.protocol.VpnException
import org.amnezia.vpn.protocol.VpnStartException import org.amnezia.vpn.protocol.VpnStartException
import org.amnezia.vpn.protocol.awg.Awg import org.amnezia.vpn.protocol.awg.Awg
import org.amnezia.vpn.protocol.cloak.Cloak import org.amnezia.vpn.protocol.cloak.Cloak
import org.amnezia.vpn.protocol.openvpn.OpenVpn import org.amnezia.vpn.protocol.openvpn.OpenVpn
import org.amnezia.vpn.protocol.putStatistics
import org.amnezia.vpn.protocol.putStatus import org.amnezia.vpn.protocol.putStatus
import org.amnezia.vpn.protocol.wireguard.Wireguard import org.amnezia.vpn.protocol.wireguard.Wireguard
import org.amnezia.vpn.util.Log import org.amnezia.vpn.util.Log
@@ -57,12 +59,16 @@ import org.json.JSONObject
private const val TAG = "AmneziaVpnService" private const val TAG = "AmneziaVpnService"
const val VPN_CONFIG = "VPN_CONFIG" const val MSG_VPN_CONFIG = "VPN_CONFIG"
const val ERROR_MSG = "ERROR_MSG" const val MSG_ERROR = "ERROR"
const val SAVE_LOGS = "SAVE_LOGS" const val MSG_SAVE_LOGS = "SAVE_LOGS"
const val MSG_CLIENT_NAME = "CLIENT_NAME"
const val AFTER_PERMISSION_CHECK = "AFTER_PERMISSION_CHECK" const val AFTER_PERMISSION_CHECK = "AFTER_PERMISSION_CHECK"
private const val PREFS_CONFIG_KEY = "LAST_CONF" private const val PREFS_CONFIG_KEY = "LAST_CONF"
private const val PREFS_SERVER_NAME = "LAST_SERVER_NAME"
private const val PREFS_SERVER_INDEX = "LAST_SERVER_INDEX"
private const val PROCESS_NAME = "org.amnezia.vpn:amneziaVpnService"
private const val NOTIFICATION_ID = 1337 private const val NOTIFICATION_ID = 1337
private const val STATISTICS_SENDING_TIMEOUT = 1000L private const val STATISTICS_SENDING_TIMEOUT = 1000L
private const val DISCONNECT_TIMEOUT = 5000L private const val DISCONNECT_TIMEOUT = 5000L
@@ -76,6 +82,8 @@ class AmneziaVpnService : VpnService() {
private var protocol: Protocol? = null private var protocol: Protocol? = null
private val protocolCache = mutableMapOf<String, Protocol>() private val protocolCache = mutableMapOf<String, Protocol>()
private var protocolState = MutableStateFlow(UNKNOWN) private var protocolState = MutableStateFlow(UNKNOWN)
private var serverName: String? = null
private var serverIndex: Int = -1
private val isConnected private val isConnected
get() = protocolState.value == CONNECTED get() = protocolState.value == CONNECTED
@@ -89,8 +97,11 @@ class AmneziaVpnService : VpnService() {
private var connectionJob: Job? = null private var connectionJob: Job? = null
private var disconnectionJob: Job? = null private var disconnectionJob: Job? = null
private var statisticsSendingJob: Job? = null private var statisticsSendingJob: Job? = null
private lateinit var clientMessenger: IpcMessenger
private lateinit var networkState: NetworkState private lateinit var networkState: NetworkState
private val clientMessengers = ConcurrentHashMap<Messenger, IpcMessenger>()
private val isActivityConnected
get() = clientMessengers.any { it.value.name == ACTIVITY_MESSENGER_NAME }
private val connectionExceptionHandler = CoroutineExceptionHandler { _, e -> private val connectionExceptionHandler = CoroutineExceptionHandler { _, e ->
protocolState.value = DISCONNECTED protocolState.value = DISCONNECTED
@@ -116,13 +127,22 @@ class AmneziaVpnService : VpnService() {
Log.d(TAG, "Handle action: $action") Log.d(TAG, "Handle action: $action")
when (action) { when (action) {
Action.REGISTER_CLIENT -> { Action.REGISTER_CLIENT -> {
clientMessenger.set(msg.replyTo) val clientName = msg.data.getString(MSG_CLIENT_NAME)
val messenger = IpcMessenger(msg.replyTo, clientName)
clientMessengers[msg.replyTo] = messenger
Log.d(TAG, "Messenger client '$clientName' was registered")
if (clientName == ACTIVITY_MESSENGER_NAME && isConnected) launchSendingStatistics()
}
Action.UNREGISTER_CLIENT -> {
clientMessengers.remove(msg.replyTo)?.let {
Log.d(TAG, "Messenger client '${it.name}' was unregistered")
if (it.name == ACTIVITY_MESSENGER_NAME) stopSendingStatistics()
}
} }
Action.CONNECT -> { Action.CONNECT -> {
val vpnConfig = msg.data.getString(VPN_CONFIG) connect(msg.data.getString(MSG_VPN_CONFIG))
Prefs.save(PREFS_CONFIG_KEY, vpnConfig)
connect(vpnConfig)
} }
Action.DISCONNECT -> { Action.DISCONNECT -> {
@@ -130,17 +150,17 @@ class AmneziaVpnService : VpnService() {
} }
Action.REQUEST_STATUS -> { Action.REQUEST_STATUS -> {
clientMessenger.send { clientMessengers[msg.replyTo]?.let { clientMessenger ->
ServiceEvent.STATUS.packToMessage { clientMessenger.send {
putStatus(Status.build { ServiceEvent.STATUS.packToMessage {
setState(this@AmneziaVpnService.protocolState.value) putStatus(this@AmneziaVpnService.protocolState.value)
}) }
} }
} }
} }
Action.SET_SAVE_LOGS -> { Action.SET_SAVE_LOGS -> {
Log.saveLogs = msg.data.getBoolean(SAVE_LOGS) Log.saveLogs = msg.data.getBoolean(MSG_SAVE_LOGS)
} }
} }
} }
@@ -189,7 +209,7 @@ class AmneziaVpnService : VpnService() {
Log.d(TAG, "Create Amnezia VPN service") Log.d(TAG, "Create Amnezia VPN service")
mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
connectionScope = CoroutineScope(SupervisorJob() + Dispatchers.IO + connectionExceptionHandler) connectionScope = CoroutineScope(SupervisorJob() + Dispatchers.IO + connectionExceptionHandler)
clientMessenger = IpcMessenger(messengerName = "Client") loadServerData()
launchProtocolStateHandler() launchProtocolStateHandler()
networkState = NetworkState(this, ::reconnect) networkState = NetworkState(this, ::reconnect)
} }
@@ -201,15 +221,13 @@ class AmneziaVpnService : VpnService() {
if (isAlwaysOnCompat) { if (isAlwaysOnCompat) {
Log.d(TAG, "Start service via Always-on") Log.d(TAG, "Start service via Always-on")
connect(Prefs.load(PREFS_CONFIG_KEY)) connect()
} else if (intent?.getBooleanExtra(AFTER_PERMISSION_CHECK, false) == true) { } else if (intent?.getBooleanExtra(AFTER_PERMISSION_CHECK, false) == true) {
Log.d(TAG, "Start service after permission check") Log.d(TAG, "Start service after permission check")
connect(Prefs.load(PREFS_CONFIG_KEY)) connect()
} else { } else {
Log.d(TAG, "Start service") Log.d(TAG, "Start service")
val vpnConfig = intent?.getStringExtra(VPN_CONFIG) connect(intent?.getStringExtra(MSG_VPN_CONFIG))
Prefs.save(PREFS_CONFIG_KEY, vpnConfig)
connect(vpnConfig)
} }
ServiceCompat.startForeground(this, NOTIFICATION_ID, notification, foregroundServiceTypeCompat) ServiceCompat.startForeground(this, NOTIFICATION_ID, notification, foregroundServiceTypeCompat)
return START_REDELIVER_INTENT return START_REDELIVER_INTENT
@@ -219,17 +237,16 @@ class AmneziaVpnService : VpnService() {
Log.d(TAG, "onBind by $intent") Log.d(TAG, "onBind by $intent")
if (intent?.action == SERVICE_INTERFACE) return super.onBind(intent) if (intent?.action == SERVICE_INTERFACE) return super.onBind(intent)
isServiceBound = true isServiceBound = true
if (isConnected) launchSendingStatistics()
return vpnServiceMessenger.binder return vpnServiceMessenger.binder
} }
override fun onUnbind(intent: Intent?): Boolean { override fun onUnbind(intent: Intent?): Boolean {
Log.d(TAG, "onUnbind by $intent") Log.d(TAG, "onUnbind by $intent")
if (intent?.action != SERVICE_INTERFACE) { if (intent?.action != SERVICE_INTERFACE) {
isServiceBound = false if (clientMessengers.isEmpty()) {
stopSendingStatistics() isServiceBound = false
clientMessenger.reset() if (isUnknown || isDisconnected) stopService()
if (isUnknown || isDisconnected) stopService() }
} }
return true return true
} }
@@ -238,7 +255,6 @@ class AmneziaVpnService : VpnService() {
Log.d(TAG, "onRebind by $intent") Log.d(TAG, "onRebind by $intent")
if (intent?.action != SERVICE_INTERFACE) { if (intent?.action != SERVICE_INTERFACE) {
isServiceBound = true isServiceBound = true
if (isConnected) launchSendingStatistics()
} }
super.onRebind(intent) super.onRebind(intent)
} }
@@ -278,17 +294,16 @@ class AmneziaVpnService : VpnService() {
*/ */
private fun launchProtocolStateHandler() { private fun launchProtocolStateHandler() {
mainScope.launch { mainScope.launch {
protocolState.collect { protocolState -> // drop first default UNKNOWN state
protocolState.drop(1).collect { protocolState ->
Log.d(TAG, "Protocol state changed: $protocolState") Log.d(TAG, "Protocol state changed: $protocolState")
when (protocolState) { when (protocolState) {
CONNECTED -> { CONNECTED -> {
clientMessenger.send(ServiceEvent.CONNECTED)
networkState.bindNetworkListener() networkState.bindNetworkListener()
if (isServiceBound) launchSendingStatistics() if (isActivityConnected) launchSendingStatistics()
} }
DISCONNECTED -> { DISCONNECTED -> {
clientMessenger.send(ServiceEvent.DISCONNECTED)
networkState.unbindNetworkListener() networkState.unbindNetworkListener()
stopSendingStatistics() stopSendingStatistics()
if (!isServiceBound) stopService() if (!isServiceBound) stopService()
@@ -300,12 +315,19 @@ class AmneziaVpnService : VpnService() {
} }
RECONNECTING -> { RECONNECTING -> {
clientMessenger.send(ServiceEvent.RECONNECTING)
stopSendingStatistics() stopSendingStatistics()
} }
CONNECTING, UNKNOWN -> {} CONNECTING, UNKNOWN -> {}
} }
clientMessengers.send {
ServiceEvent.STATUS_CHANGED.packToMessage {
putStatus(protocolState)
}
}
VpnStateStore.store { VpnState(protocolState, serverName, serverIndex) }
} }
} }
} }
@@ -332,7 +354,17 @@ class AmneziaVpnService : VpnService() {
} }
@MainThread @MainThread
private fun connect(vpnConfig: String?) { private fun connect(vpnConfig: String? = null) {
if (vpnConfig == null) {
connectToVpn(Prefs.load(PREFS_CONFIG_KEY))
} else {
Prefs.save(PREFS_CONFIG_KEY, vpnConfig)
connectToVpn(vpnConfig)
}
}
@MainThread
private fun connectToVpn(vpnConfig: String) {
if (isConnected || protocolState.value == CONNECTING) return if (isConnected || protocolState.value == CONNECTING) return
Log.d(TAG, "Start VPN connection") Log.d(TAG, "Start VPN connection")
@@ -340,6 +372,7 @@ class AmneziaVpnService : VpnService() {
protocolState.value = CONNECTING protocolState.value = CONNECTING
val config = parseConfigToJson(vpnConfig) val config = parseConfigToJson(vpnConfig)
saveServerData(config)
if (config == null) { if (config == null) {
onError("Invalid VPN config") onError("Invalid VPN config")
protocolState.value = DISCONNECTED protocolState.value = DISCONNECTED
@@ -417,24 +450,38 @@ class AmneziaVpnService : VpnService() {
private fun onError(msg: String) { private fun onError(msg: String) {
Log.e(TAG, msg) Log.e(TAG, msg)
mainScope.launch { mainScope.launch {
clientMessenger.send { clientMessengers.send {
ServiceEvent.ERROR.packToMessage { ServiceEvent.ERROR.packToMessage {
putString(ERROR_MSG, msg) putString(MSG_ERROR, msg)
} }
} }
} }
} }
private fun parseConfigToJson(vpnConfig: String?): JSONObject? = private fun parseConfigToJson(vpnConfig: String): JSONObject? =
try { if (vpnConfig.isBlank()) {
vpnConfig?.let {
JSONObject(it)
}
} catch (e: JSONException) {
onError("Invalid VPN config json format: ${e.message}")
null null
} else {
try {
JSONObject(vpnConfig)
} catch (e: JSONException) {
onError("Invalid VPN config json format: ${e.message}")
null
}
} }
private fun saveServerData(config: JSONObject?) {
serverName = config?.opt("description") as String?
serverIndex = config?.opt("serverIndex") as Int? ?: -1
Prefs.save(PREFS_SERVER_NAME, serverName)
Prefs.save(PREFS_SERVER_INDEX, serverIndex)
}
private fun loadServerData() {
serverName = Prefs.load<String>(PREFS_SERVER_NAME).ifBlank { null }
if (serverName != null) serverIndex = Prefs.load(PREFS_SERVER_INDEX)
}
private fun checkPermission(): Boolean = private fun checkPermission(): Boolean =
if (prepare(applicationContext) != null) { if (prepare(applicationContext) != null) {
Intent(this, VpnRequestActivity::class.java).apply { Intent(this, VpnRequestActivity::class.java).apply {
@@ -446,4 +493,12 @@ class AmneziaVpnService : VpnService() {
} else { } else {
true true
} }
companion object {
fun isRunning(context: Context): Boolean =
(context.getSystemService(ACTIVITY_SERVICE) as ActivityManager)
.runningAppProcesses.any {
it.processName == PROCESS_NAME && it.importance <= IMPORTANCE_FOREGROUND_SERVICE
}
}
} }
@@ -20,9 +20,7 @@ sealed interface IpcMessage {
} }
enum class ServiceEvent : IpcMessage { enum class ServiceEvent : IpcMessage {
CONNECTED, STATUS_CHANGED,
DISCONNECTED,
RECONNECTING,
STATUS, STATUS,
STATISTICS_UPDATE, STATISTICS_UPDATE,
ERROR ERROR
@@ -30,6 +28,7 @@ enum class ServiceEvent : IpcMessage {
enum class Action : IpcMessage { enum class Action : IpcMessage {
REGISTER_CLIENT, REGISTER_CLIENT,
UNREGISTER_CLIENT,
CONNECT, CONNECT,
DISCONNECT, DISCONNECT,
REQUEST_STATUS, REQUEST_STATUS,
@@ -9,11 +9,21 @@ import org.amnezia.vpn.util.Log
private const val TAG = "IpcMessenger" private const val TAG = "IpcMessenger"
class IpcMessenger( class IpcMessenger(
messengerName: String? = null,
private val onDeadObjectException: () -> Unit = {}, private val onDeadObjectException: () -> Unit = {},
private val onRemoteException: () -> Unit = {}, private val onRemoteException: () -> Unit = {}
private val messengerName: String = "Unknown"
) { ) {
private var messenger: Messenger? = null private var messenger: Messenger? = null
val name = messengerName ?: "Unknown"
constructor(
messenger: Messenger,
messengerName: String? = null,
onDeadObjectException: () -> Unit = {},
onRemoteException: () -> Unit = {}
) : this(messengerName, onDeadObjectException, onRemoteException) {
this.messenger = messenger
}
fun set(messenger: Messenger) { fun set(messenger: Messenger) {
this.messenger = messenger this.messenger = messenger
@@ -25,19 +35,29 @@ class IpcMessenger(
fun send(msg: () -> Message) = messenger?.sendMsg(msg()) fun send(msg: () -> Message) = messenger?.sendMsg(msg())
fun send(msg: Message, replyTo: Messenger) = messenger?.sendMsg(msg.apply { this.replyTo = replyTo })
fun <T> send(msg: T) fun <T> send(msg: T)
where T : Enum<T>, T : IpcMessage = messenger?.sendMsg(msg.packToMessage()) where T : Enum<T>, T : IpcMessage = messenger?.sendMsg(msg.packToMessage())
fun <T> send(msg: T, replyTo: Messenger)
where T : Enum<T>, T : IpcMessage = messenger?.sendMsg(msg.packToMessage().apply { this.replyTo = replyTo })
private fun Messenger.sendMsg(msg: Message) { private fun Messenger.sendMsg(msg: Message) {
try { try {
send(msg) send(msg)
} catch (e: DeadObjectException) { } catch (e: DeadObjectException) {
Log.w(TAG, "$messengerName messenger is dead") Log.w(TAG, "$name messenger is dead")
messenger = null messenger = null
onDeadObjectException() onDeadObjectException()
} catch (e: RemoteException) { } catch (e: RemoteException) {
Log.w(TAG, "Sending a message to the $messengerName messenger failed: ${e.message}") Log.w(TAG, "Sending a message to the $name messenger failed: ${e.message}")
onRemoteException() onRemoteException()
} }
} }
} }
fun Map<Messenger, IpcMessenger>.send(msg: () -> Message) = this.values.forEach { it.send(msg) }
fun <T> Map<Messenger, IpcMessenger>.send(msg: T)
where T : Enum<T>, T : IpcMessage = this.values.forEach { it.send(msg) }
@@ -0,0 +1,75 @@
package org.amnezia.vpn
import android.app.Application
import androidx.datastore.core.MultiProcessDataStoreFactory
import androidx.datastore.core.Serializer
import androidx.datastore.dataStoreFile
import java.io.InputStream
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.io.OutputStream
import java.io.Serializable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
import org.amnezia.vpn.protocol.ProtocolState
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
import org.amnezia.vpn.util.Log
private const val TAG = "VpnState"
private const val STORE_FILE_NAME = "vpnState"
data class VpnState(
val protocolState: ProtocolState,
val serverName: String? = null,
val serverIndex: Int = -1
) : Serializable {
companion object {
private const val serialVersionUID: Long = -1760654961004181606
val defaultState: VpnState = VpnState(DISCONNECTED)
}
}
object VpnStateStore {
private lateinit var app: Application
private val dataStore = MultiProcessDataStoreFactory.create(
serializer = VpnStateSerializer(),
produceFile = { app.dataStoreFile(STORE_FILE_NAME) }
)
fun init(app: Application) {
Log.v(TAG, "Init VpnStateStore")
this.app = app
}
fun dataFlow(): Flow<VpnState> = dataStore.data
suspend fun store(f: (vpnState: VpnState) -> VpnState) {
try {
dataStore.updateData(f)
} catch (e : Exception) {
Log.e(TAG, "Failed to store VpnState: $e")
}
}
}
private class VpnStateSerializer : Serializer<VpnState> {
override val defaultValue: VpnState = VpnState.defaultState
override suspend fun readFrom(input: InputStream): VpnState {
return withContext(Dispatchers.IO) {
ObjectInputStream(input).use {
it.readObject() as VpnState
}
}
}
override suspend fun writeTo(t: VpnState, output: OutputStream) {
withContext(Dispatchers.IO) {
ObjectOutputStream(output).use {
it.writeObject(t)
}
}
}
}
@@ -1,18 +1,23 @@
package org.amnezia.vpn.qt package org.amnezia.vpn.qt
import org.amnezia.vpn.protocol.ProtocolState
import org.amnezia.vpn.protocol.Status
/** /**
* JNI functions of the AndroidController class from android_controller.cpp, * JNI functions of the AndroidController class from android_controller.cpp,
* called by events in the Android part of the client * called by events in the Android part of the client
*/ */
object QtAndroidController { object QtAndroidController {
fun onStatus(status: Status) = onStatus(status.state)
fun onStatus(protocolState: ProtocolState) = onStatus(protocolState.ordinal)
external fun onStatus(stateCode: Int) external fun onStatus(stateCode: Int)
external fun onServiceDisconnected() external fun onServiceDisconnected()
external fun onServiceError() external fun onServiceError()
external fun onVpnPermissionRejected() external fun onVpnPermissionRejected()
external fun onVpnConnected() external fun onVpnStateChanged(stateCode: Int)
external fun onVpnDisconnected()
external fun onVpnReconnecting()
external fun onStatisticsUpdate(rxBytes: Long, txBytes: Long) external fun onStatisticsUpdate(rxBytes: Long, txBytes: Long)
external fun onFileOpened(uri: String) external fun onFileOpened(uri: String)
-4
View File
@@ -48,10 +48,6 @@ int main(int argc, char *argv[])
AllowSetForegroundWindow(0); AllowSetForegroundWindow(0);
#endif #endif
#if defined(Q_OS_IOS)
QtAppDelegateInitialize();
#endif
app.registerTypes(); app.registerTypes();
app.setApplicationName(APPLICATION_NAME); app.setApplicationName(APPLICATION_NAME);
+1 -1
View File
@@ -125,7 +125,7 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
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));
// todo review wg ipv6 // todo review wg ipv6
#ifndef Q_OS_WINDOWS #ifdef Q_OS_MACOS
json.insert("deviceIpv6Address", "dead::1"); json.insert("deviceIpv6Address", "dead::1");
#endif #endif
json.insert("serverPublicKey", wgConfig.value(amnezia::config_key::server_pub_key)); json.insert("serverPublicKey", wgConfig.value(amnezia::config_key::server_pub_key));
+13 -42
View File
@@ -56,26 +56,10 @@ AndroidController::AndroidController() : QObject()
Qt::QueuedConnection); Qt::QueuedConnection);
connect( connect(
this, &AndroidController::vpnConnected, this, this, &AndroidController::vpnStateChanged, this,
[this]() { [this](AndroidController::ConnectionState state) {
qDebug() << "Android event: VPN connected"; qDebug() << "Android event: VPN state changed:" << textConnectionState(state);
emit connectionStateChanged(Vpn::ConnectionState::Connected); emit connectionStateChanged(convertState(state));
},
Qt::QueuedConnection);
connect(
this, &AndroidController::vpnDisconnected, this,
[this]() {
qDebug() << "Android event: VPN disconnected";
emit connectionStateChanged(Vpn::ConnectionState::Disconnected);
},
Qt::QueuedConnection);
connect(
this, &AndroidController::vpnReconnecting, this,
[this]() {
qDebug() << "Android event: VPN reconnecting";
emit connectionStateChanged(Vpn::ConnectionState::Reconnecting);
}, },
Qt::QueuedConnection); Qt::QueuedConnection);
@@ -106,9 +90,7 @@ bool AndroidController::initialize()
{"onServiceDisconnected", "()V", reinterpret_cast<void *>(onServiceDisconnected)}, {"onServiceDisconnected", "()V", reinterpret_cast<void *>(onServiceDisconnected)},
{"onServiceError", "()V", reinterpret_cast<void *>(onServiceError)}, {"onServiceError", "()V", reinterpret_cast<void *>(onServiceError)},
{"onVpnPermissionRejected", "()V", reinterpret_cast<void *>(onVpnPermissionRejected)}, {"onVpnPermissionRejected", "()V", reinterpret_cast<void *>(onVpnPermissionRejected)},
{"onVpnConnected", "()V", reinterpret_cast<void *>(onVpnConnected)}, {"onVpnStateChanged", "(I)V", reinterpret_cast<void *>(onVpnStateChanged)},
{"onVpnDisconnected", "()V", reinterpret_cast<void *>(onVpnDisconnected)},
{"onVpnReconnecting", "()V", reinterpret_cast<void *>(onVpnReconnecting)},
{"onStatisticsUpdate", "(JJ)V", reinterpret_cast<void *>(onStatisticsUpdate)}, {"onStatisticsUpdate", "(JJ)V", reinterpret_cast<void *>(onStatisticsUpdate)},
{"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)},
@@ -158,6 +140,11 @@ void AndroidController::stop()
callActivityMethod("stop", "()V"); callActivityMethod("stop", "()V");
} }
void AndroidController::resetLastServer(int serverIndex)
{
callActivityMethod("resetLastServer", "(I)V", serverIndex);
}
void AndroidController::saveFile(const QString &fileName, const QString &data) void AndroidController::saveFile(const QString &fileName, const QString &data)
{ {
callActivityMethod("saveFile", "(Ljava/lang/String;Ljava/lang/String;)V", callActivityMethod("saveFile", "(Ljava/lang/String;Ljava/lang/String;)V",
@@ -370,30 +357,14 @@ void AndroidController::onVpnPermissionRejected(JNIEnv *env, jobject thiz)
} }
// static // static
void AndroidController::onVpnConnected(JNIEnv *env, jobject thiz) void AndroidController::onVpnStateChanged(JNIEnv *env, jobject thiz, jint stateCode)
{ {
Q_UNUSED(env); Q_UNUSED(env);
Q_UNUSED(thiz); Q_UNUSED(thiz);
emit AndroidController::instance()->vpnConnected(); auto state = ConnectionState(stateCode);
}
// static emit AndroidController::instance()->vpnStateChanged(state);
void AndroidController::onVpnDisconnected(JNIEnv *env, jobject thiz)
{
Q_UNUSED(env);
Q_UNUSED(thiz);
emit AndroidController::instance()->vpnDisconnected();
}
// static
void AndroidController::onVpnReconnecting(JNIEnv *env, jobject thiz)
{
Q_UNUSED(env);
Q_UNUSED(thiz);
emit AndroidController::instance()->vpnReconnecting();
} }
// static // static
@@ -20,9 +20,9 @@ public:
// keep synchronized with org.amnezia.vpn.protocol.ProtocolState // keep synchronized with org.amnezia.vpn.protocol.ProtocolState
enum class ConnectionState enum class ConnectionState
{ {
DISCONNECTED,
CONNECTED, CONNECTED,
CONNECTING, CONNECTING,
DISCONNECTED,
DISCONNECTING, DISCONNECTING,
RECONNECTING, RECONNECTING,
UNKNOWN UNKNOWN
@@ -30,6 +30,7 @@ public:
ErrorCode start(const QJsonObject &vpnConfig); ErrorCode start(const QJsonObject &vpnConfig);
void stop(); void stop();
void resetLastServer(int serverIndex);
void setNotificationText(const QString &title, const QString &message, int timerSec); void setNotificationText(const QString &title, const QString &message, int timerSec);
void saveFile(const QString &fileName, const QString &data); void saveFile(const QString &fileName, const QString &data);
QString openFile(const QString &filter); QString openFile(const QString &filter);
@@ -48,9 +49,7 @@ signals:
void serviceDisconnected(); void serviceDisconnected();
void serviceError(); void serviceError();
void vpnPermissionRejected(); void vpnPermissionRejected();
void vpnConnected(); void vpnStateChanged(ConnectionState state);
void vpnDisconnected();
void vpnReconnecting();
void statisticsUpdated(quint64 rxBytes, quint64 txBytes); void statisticsUpdated(quint64 rxBytes, quint64 txBytes);
void fileOpened(QString uri); void fileOpened(QString uri);
void configImported(QString config); void configImported(QString config);
@@ -77,9 +76,7 @@ private:
static void onServiceDisconnected(JNIEnv *env, jobject thiz); static void onServiceDisconnected(JNIEnv *env, jobject thiz);
static void onServiceError(JNIEnv *env, jobject thiz); static void onServiceError(JNIEnv *env, jobject thiz);
static void onVpnPermissionRejected(JNIEnv *env, jobject thiz); static void onVpnPermissionRejected(JNIEnv *env, jobject thiz);
static void onVpnConnected(JNIEnv *env, jobject thiz); static void onVpnStateChanged(JNIEnv *env, jobject thiz, jint stateCode);
static void onVpnDisconnected(JNIEnv *env, jobject thiz);
static void onVpnReconnecting(JNIEnv *env, jobject thiz);
static void onStatisticsUpdate(JNIEnv *env, jobject thiz, jlong rxBytes, jlong txBytes); static void onStatisticsUpdate(JNIEnv *env, jobject thiz, jlong rxBytes, jlong txBytes);
static void onConfigImported(JNIEnv *env, jobject thiz, jstring data); static void onConfigImported(JNIEnv *env, jobject thiz, jstring data);
static void onFileOpened(JNIEnv *env, jobject thiz, jstring uri); static void onFileOpened(JNIEnv *env, jobject thiz, jstring uri);
+24
View File
@@ -1,4 +1,5 @@
import Foundation import Foundation
import NetworkExtension
public func swiftUpdateLogData(_ qtString: std.string) -> std.string { public func swiftUpdateLogData(_ qtString: std.string) -> std.string {
let qtLog = Log(String(describing: qtString)) let qtLog = Log(String(describing: qtString))
@@ -24,3 +25,26 @@ public func swiftDeleteLog() {
public func toggleLogging(_ isEnabled: Bool) { public func toggleLogging(_ isEnabled: Bool) {
Log.isLoggingEnabled = isEnabled Log.isLoggingEnabled = isEnabled
} }
public func clearSettings() {
NETunnelProviderManager.loadAllFromPreferences { managers, error in
if let error {
NSLog("clearSettings removeFromPreferences error: \(error.localizedDescription)")
return
}
managers?.forEach { manager in
manager.removeFromPreferences { error in
if let error {
NSLog("NE removeFromPreferences error: \(error.localizedDescription)")
} else {
manager.loadFromPreferences { error in
if let error {
NSLog("NE loadFromPreferences after remove error: \(error.localizedDescription)")
}
}
}
}
}
}
}
@@ -18,40 +18,32 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
// send empty string to NEDNSSettings.matchDomains // send empty string to NEDNSSettings.matchDomains
networkSettings?.dnsSettings?.matchDomains = [""] networkSettings?.dnsSettings?.matchDomains = [""]
if splitTunnelType == "1" { if splitTunnelType == 1 {
var ipv4IncludedRoutes = [NEIPv4Route]() var ipv4IncludedRoutes = [NEIPv4Route]()
let STSdata = Data(splitTunnelSites!.utf8)
do { for allowedIPString in splitTunnelSites {
guard let STSArray = try JSONSerialization.jsonObject(with: STSdata) as? [String] else { return } if let allowedIP = IPAddressRange(from: allowedIPString) {
for allowedIPString in STSArray { ipv4IncludedRoutes.append(NEIPv4Route(
if let allowedIP = IPAddressRange(from: allowedIPString) { destinationAddress: "\(allowedIP.address)",
ipv4IncludedRoutes.append(NEIPv4Route( subnetMask: "\(allowedIP.subnetMask())"))
destinationAddress: "\(allowedIP.address)",
subnetMask: "\(allowedIP.subnetMask())"))
}
} }
} catch {
wg_log(.error, message: "Parse JSONSerialization Error")
} }
networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
} else { } else {
if splitTunnelType == "2" { if splitTunnelType == 2 {
var ipv4ExcludedRoutes = [NEIPv4Route]() var ipv4ExcludedRoutes = [NEIPv4Route]()
var ipv4IncludedRoutes = [NEIPv4Route]() var ipv4IncludedRoutes = [NEIPv4Route]()
var ipv6IncludedRoutes = [NEIPv6Route]() var ipv6IncludedRoutes = [NEIPv6Route]()
let STSdata = Data(splitTunnelSites!.utf8)
do { for excludeIPString in splitTunnelSites {
guard let STSArray = try JSONSerialization.jsonObject(with: STSdata) as? [String] else { return } if let excludeIP = IPAddressRange(from: excludeIPString) {
for excludeIPString in STSArray { ipv4ExcludedRoutes.append(NEIPv4Route(
if let excludeIP = IPAddressRange(from: excludeIPString) { destinationAddress: "\(excludeIP.address)",
ipv4ExcludedRoutes.append(NEIPv4Route( subnetMask: "\(excludeIP.subnetMask())"))
destinationAddress: "\(excludeIP.address)",
subnetMask: "\(excludeIP.subnetMask())"))
}
} }
} catch {
wg_log(.error, message: "Parse JSONSerialization Error")
} }
if let allIPv4 = IPAddressRange(from: "0.0.0.0/0") { if let allIPv4 = IPAddressRange(from: "0.0.0.0/0") {
ipv4IncludedRoutes.append(NEIPv4Route( ipv4IncludedRoutes.append(NEIPv4Route(
destinationAddress: "\(allIPv4.address)", destinationAddress: "\(allIPv4.address)",
+82 -90
View File
@@ -50,8 +50,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
private let dispatchQueue = DispatchQueue(label: "PacketTunnel", qos: .utility) private let dispatchQueue = DispatchQueue(label: "PacketTunnel", qos: .utility)
private var openVPNConfig: Data? private var openVPNConfig: Data?
var splitTunnelType: String? var splitTunnelType: Int!
var splitTunnelSites: String? var splitTunnelSites: [String]!
let vpnReachability = OpenVPNReachability() let vpnReachability = OpenVPNReachability()
@@ -81,22 +81,6 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
if action == Constants.kActionStatus { if action == Constants.kActionStatus {
handleStatusAppMessage(messageData, completionHandler: completionHandler) handleStatusAppMessage(messageData, completionHandler: completionHandler)
} }
if action == Constants.kActionStart {
splitTunnelType = message[Constants.kMessageKeySplitTunnelType] as? String
splitTunnelSites = message[Constants.kMessageKeySplitTunnelSites] as? String
}
let callbackWrapper: (NSNumber?) -> Void = { errorCode in
// let tunnelId = self.tunnelConfig?.id ?? ""
let response: [String: Any] = [
Constants.kMessageKeyAction: action,
Constants.kMessageKeyErrorCode: errorCode ?? NSNull(),
Constants.kMessageKeyTunnelId: 0
]
completionHandler(try? JSONSerialization.data(withJSONObject: response, options: []))
}
} }
override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) { override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) {
@@ -169,110 +153,118 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
completionHandler: @escaping (Error?) -> Void) { completionHandler: @escaping (Error?) -> Void) {
guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol, guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol,
let providerConfiguration = protocolConfiguration.providerConfiguration, let providerConfiguration = protocolConfiguration.providerConfiguration,
let wgConfig: Data = providerConfiguration[Constants.wireGuardConfigKey] as? Data else { let wgConfigData: Data = providerConfiguration[Constants.wireGuardConfigKey] as? Data else {
wg_log(.error, message: "Can't start WireGuard config missing") wg_log(.error, message: "Can't start WireGuard config missing")
completionHandler(nil) completionHandler(nil)
return return
} }
guard let wgConfigStr = try? JSONDecoder().decode(WGConfig.self, from: wgConfig).str, do {
let tunnelConfiguration = try? TunnelConfiguration(fromWgQuickConfig: wgConfigStr) let wgConfig = try JSONDecoder().decode(WGConfig.self, from: wgConfigData)
else { let wgConfigStr = wgConfig.str
wg_log(.error, message: "Can't parse WireGuard config") log(.info, message: "wgConfig: \(wgConfig.redux.replacingOccurrences(of: "\n", with: " "))")
completionHandler(nil)
return
}
log(.info, message: "wgConfig: \(wgConfigStr.replacingOccurrences(of: "\n", with: " "))") let tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: wgConfigStr)
if tunnelConfiguration.peers.first!.allowedIPs if tunnelConfiguration.peers.first!.allowedIPs
.map({ $0.stringRepresentation }) .map({ $0.stringRepresentation })
.joined(separator: ", ") == "0.0.0.0/0, ::/0" { .joined(separator: ", ") == "0.0.0.0/0, ::/0" {
if splitTunnelType == "1" { if wgConfig.splitTunnelType == 1 {
for index in tunnelConfiguration.peers.indices { for index in tunnelConfiguration.peers.indices {
tunnelConfiguration.peers[index].allowedIPs.removeAll() tunnelConfiguration.peers[index].allowedIPs.removeAll()
var allowedIPs = [IPAddressRange]() var allowedIPs = [IPAddressRange]()
let STSdata = Data(splitTunnelSites!.utf8)
do { for allowedIPString in wgConfig.splitTunnelSites {
guard let STSArray = try JSONSerialization.jsonObject(with: STSdata) as? [String] else { return }
for allowedIPString in STSArray {
if let allowedIP = IPAddressRange(from: allowedIPString) { if let allowedIP = IPAddressRange(from: allowedIPString) {
allowedIPs.append(allowedIP) allowedIPs.append(allowedIP)
} }
} }
} catch {
wg_log(.error, message: "Parse JSONSerialization Error") tunnelConfiguration.peers[index].allowedIPs = allowedIPs
} }
tunnelConfiguration.peers[index].allowedIPs = allowedIPs } else if wgConfig.splitTunnelType == 2 {
} for index in tunnelConfiguration.peers.indices {
} else if splitTunnelType == "2" { var excludeIPs = [IPAddressRange]()
for index in tunnelConfiguration.peers.indices {
var excludeIPs = [IPAddressRange]() for excludeIPString in wgConfig.splitTunnelSites {
let STSdata = Data(splitTunnelSites!.utf8)
do {
guard let STSArray = try JSONSerialization.jsonObject(with: STSdata) as? [String] else { return }
for excludeIPString in STSArray {
if let excludeIP = IPAddressRange(from: excludeIPString) { if let excludeIP = IPAddressRange(from: excludeIPString) {
excludeIPs.append(excludeIP) excludeIPs.append(excludeIP)
} }
} }
} catch {
wg_log(.error, message: "Parse JSONSerialization Error") tunnelConfiguration.peers[index].excludeIPs = excludeIPs
} }
tunnelConfiguration.peers[index].excludeIPs = excludeIPs
} }
} }
}
wg_log(.info, message: "Starting wireguard tunnel from the " + wg_log(.info, message: "Starting wireguard tunnel from the " +
(activationAttemptId == nil ? "OS directly, rather than the app" : "app")) (activationAttemptId == nil ? "OS directly, rather than the app" : "app"))
// Start the tunnel // Start the tunnel
wgAdapter.start(tunnelConfiguration: tunnelConfiguration) { adapterError in wgAdapter.start(tunnelConfiguration: tunnelConfiguration) { adapterError in
guard let adapterError else { guard let adapterError else {
let interfaceName = self.wgAdapter.interfaceName ?? "unknown" let interfaceName = self.wgAdapter.interfaceName ?? "unknown"
wg_log(.info, message: "Tunnel interface is \(interfaceName)") wg_log(.info, message: "Tunnel interface is \(interfaceName)")
completionHandler(nil) completionHandler(nil)
return return
} }
switch adapterError { switch adapterError {
case .cannotLocateTunnelFileDescriptor: case .cannotLocateTunnelFileDescriptor:
wg_log(.error, staticMessage: "Starting tunnel failed: could not determine file descriptor") wg_log(.error, staticMessage: "Starting tunnel failed: could not determine file descriptor")
errorNotifier.notify(PacketTunnelProviderError.couldNotDetermineFileDescriptor) errorNotifier.notify(PacketTunnelProviderError.couldNotDetermineFileDescriptor)
completionHandler(PacketTunnelProviderError.couldNotDetermineFileDescriptor) completionHandler(PacketTunnelProviderError.couldNotDetermineFileDescriptor)
case .dnsResolution(let dnsErrors): case .dnsResolution(let dnsErrors):
let hostnamesWithDnsResolutionFailure = dnsErrors.map { $0.address } let hostnamesWithDnsResolutionFailure = dnsErrors.map { $0.address }
.joined(separator: ", ") .joined(separator: ", ")
wg_log(.error, message: wg_log(.error, message:
"DNS resolution failed for the following hostnames: \(hostnamesWithDnsResolutionFailure)") "DNS resolution failed for the following hostnames: \(hostnamesWithDnsResolutionFailure)")
errorNotifier.notify(PacketTunnelProviderError.dnsResolutionFailure) errorNotifier.notify(PacketTunnelProviderError.dnsResolutionFailure)
completionHandler(PacketTunnelProviderError.dnsResolutionFailure) completionHandler(PacketTunnelProviderError.dnsResolutionFailure)
case .setNetworkSettings(let error): case .setNetworkSettings(let error):
wg_log(.error, message: wg_log(.error, message:
"Starting tunnel failed with setTunnelNetworkSettings returning \(error.localizedDescription)") "Starting tunnel failed with setTunnelNetworkSettings returning \(error.localizedDescription)")
errorNotifier.notify(PacketTunnelProviderError.couldNotSetNetworkSettings) errorNotifier.notify(PacketTunnelProviderError.couldNotSetNetworkSettings)
completionHandler(PacketTunnelProviderError.couldNotSetNetworkSettings) completionHandler(PacketTunnelProviderError.couldNotSetNetworkSettings)
case .startWireGuardBackend(let errorCode): case .startWireGuardBackend(let errorCode):
wg_log(.error, message: "Starting tunnel failed with wgTurnOn returning \(errorCode)") wg_log(.error, message: "Starting tunnel failed with wgTurnOn returning \(errorCode)")
errorNotifier.notify(PacketTunnelProviderError.couldNotStartBackend) errorNotifier.notify(PacketTunnelProviderError.couldNotStartBackend)
completionHandler(PacketTunnelProviderError.couldNotStartBackend) completionHandler(PacketTunnelProviderError.couldNotStartBackend)
case .invalidState: case .invalidState:
fatalError() fatalError()
}
} }
} catch {
log(.error, message: "Can't parse WG config: \(error.localizedDescription)")
completionHandler(nil)
return
} }
} }
private func startOpenVPN(completionHandler: @escaping (Error?) -> Void) { private func startOpenVPN(completionHandler: @escaping (Error?) -> Void) {
guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol, guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol,
let providerConfiguration = protocolConfiguration.providerConfiguration, let providerConfiguration = protocolConfiguration.providerConfiguration,
let ovpnConfiguration: Data = providerConfiguration[Constants.ovpnConfigKey] as? Data else { let openVPNConfigData = providerConfiguration[Constants.ovpnConfigKey] as? Data else {
wg_log(.error, message: "Can't start startOpenVPN()") wg_log(.error, message: "Can't start startOpenVPN()")
return return
} }
setupAndlaunchOpenVPN(withConfig: ovpnConfiguration, completionHandler: completionHandler) do {
log(.info, message: "providerConfiguration: \(String(decoding: openVPNConfigData, as: UTF8.self).replacingOccurrences(of: "\n", with: " "))")
let openVPNConfig = try JSONDecoder().decode(OpenVPNConfig.self, from: openVPNConfigData)
log(.info, message: "openVPNConfig: \(openVPNConfig.str.replacingOccurrences(of: "\n", with: " "))")
let ovpnConfiguration = Data(openVPNConfig.config.utf8)
setupAndlaunchOpenVPN(withConfig: ovpnConfiguration, completionHandler: completionHandler)
} catch {
log(.error, message: "Can't parse OpenVPN config: \(error.localizedDescription)")
if let underlyingError = (error as NSError).userInfo[NSUnderlyingErrorKey] as? NSError {
log(.error, message: "Can't parse OpenVPN config: \(underlyingError.localizedDescription)")
}
return
}
} }
private func stopWireguard(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { private func stopWireguard(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
+4 -1
View File
@@ -1,4 +1,7 @@
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
@interface QtAppDelegate : UIResponder <UIApplicationDelegate> @interface QIOSApplicationDelegate
@end
@interface QIOSApplicationDelegate (AmneziaVPNDelegate)
@end @end
+16 -30
View File
@@ -3,25 +3,15 @@
#include <QFile> #include <QFile>
@implementation QtAppDelegate { UIView *_screen;
UIView *_screen;
}
+(QtAppDelegate *)sharedQtAppDelegate {
static dispatch_once_t pred;
static QtAppDelegate *shared = nil;
dispatch_once(&pred, ^{
shared = [[super alloc] init];
});
return shared;
}
@implementation QIOSApplicationDelegate (AmneziaVPNDelegate)
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{ {
[application setMinimumBackgroundFetchInterval: UIApplicationBackgroundFetchIntervalMinimum]; [application setMinimumBackgroundFetchInterval: UIApplicationBackgroundFetchIntervalMinimum];
// Override point for customization after application launch. // Override point for customization after application launch.
NSLog(@"Did this launch option happen"); NSLog(@"Application didFinishLaunchingWithOptions");
return YES; return YES;
} }
@@ -70,31 +60,27 @@
- (BOOL)application:(UIApplication *)app - (BOOL)application:(UIApplication *)app
openURL:(NSURL *)url openURL:(NSURL *)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options { options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
NSLog(@"Application openURL: %@", url);
if (url.fileURL) { if (url.fileURL) {
QString filePath(url.path.UTF8String); QString filePath(url.path.UTF8String);
if (filePath.isEmpty()) return NO; if (filePath.isEmpty()) return NO;
if (filePath.contains("backup")) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
IosController::Instance()->importBackupFromOutside(filePath); NSLog(@"Application openURL: %@", url);
} else {
QFile file(filePath); if (filePath.contains("backup")) {
bool isOpenFile = file.open(QIODevice::ReadOnly); IosController::Instance()->importBackupFromOutside(filePath);
QByteArray data = file.readAll(); } else {
QFile file(filePath);
bool isOpenFile = file.open(QIODevice::ReadOnly);
QByteArray data = file.readAll();
IosController::Instance()->importConfigFromOutside(QString(data));
}
});
IosController::Instance()->importConfigFromOutside(QString(data));
}
return YES; return YES;
} }
return NO; return NO;
} }
void QtAppDelegateInitialize()
{
[[UIApplication sharedApplication] setDelegate: [QtAppDelegate sharedQtAppDelegate]];
NSLog(@"Created a new AppDelegate");
}
@end @end
+66 -104
View File
@@ -1,10 +1,41 @@
import Foundation import Foundation
struct WGConfigData: Decodable { struct WGConfig: Decodable {
let initPacketMagicHeader, responsePacketMagicHeader: String? let initPacketMagicHeader, responsePacketMagicHeader: String?
let underloadPacketMagicHeader, transportPacketMagicHeader: String? let underloadPacketMagicHeader, transportPacketMagicHeader: String?
let junkPacketCount, junkPacketMinSize, junkPacketMaxSize: String? let junkPacketCount, junkPacketMinSize, junkPacketMaxSize: String?
let initPacketJunkSize, responsePacketJunkSize: String? let initPacketJunkSize, responsePacketJunkSize: String?
let dns1: String
let dns2: String
let hostName: String
let port: Int
let clientIP: String
let clientPrivateKey: String
let serverPublicKey: String
let presharedKey: String
var allowedIPs: [String]
var persistentKeepAlive: String
let splitTunnelType: Int
let splitTunnelSites: [String]
enum CodingKeys: String, CodingKey {
case initPacketMagicHeader = "H1", responsePacketMagicHeader = "H2"
case underloadPacketMagicHeader = "H3", transportPacketMagicHeader = "H4"
case junkPacketCount = "Jc", junkPacketMinSize = "Jmin", junkPacketMaxSize = "Jmax"
case initPacketJunkSize = "S1", responsePacketJunkSize = "S2"
case dns1
case dns2
case hostName
case port
case clientIP = "client_ip"
case clientPrivateKey = "client_priv_key"
case serverPublicKey = "server_pub_key"
case presharedKey = "psk_key"
case allowedIPs = "allowed_ips"
case persistentKeepAlive = "persistent_keep_alive"
case splitTunnelType
case splitTunnelSites
}
var settings: String { var settings: String {
junkPacketCount == nil ? "" : junkPacketCount == nil ? "" :
@@ -22,114 +53,45 @@ struct WGConfigData: Decodable {
""" """
} }
let clientIP: String
let clientPrivateKey: String
let clientPublicKey: String
let serverPublicKey: String
let presharedKey: String
let hostName: String
let port: Int
var allowedIPs: [String]
var persistentKeepAlive: String
enum CodingKeys: String, CodingKey {
case initPacketMagicHeader = "H1", responsePacketMagicHeader = "H2"
case underloadPacketMagicHeader = "H3", transportPacketMagicHeader = "H4"
case junkPacketCount = "Jc", junkPacketMinSize = "Jmin", junkPacketMaxSize = "Jmax"
case initPacketJunkSize = "S1", responsePacketJunkSize = "S2"
case clientIP = "client_ip" // "10.8.1.16"
case clientPrivateKey = "client_priv_key"
case clientPublicKey = "client_pub_key"
case serverPublicKey = "server_pub_key"
case presharedKey = "psk_key"
case allowedIPs = "allowed_ips"
case persistentKeepAlive = "persistent_keep_alive"
case hostName
case port
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.initPacketMagicHeader = try container.decodeIfPresent(String.self, forKey: .initPacketMagicHeader)
self.responsePacketMagicHeader = try container.decodeIfPresent(String.self, forKey: .responsePacketMagicHeader)
self.underloadPacketMagicHeader = try container.decodeIfPresent(String.self, forKey: .underloadPacketMagicHeader)
self.transportPacketMagicHeader = try container.decodeIfPresent(String.self, forKey: .transportPacketMagicHeader)
self.junkPacketCount = try container.decodeIfPresent(String.self, forKey: .junkPacketCount)
self.junkPacketMinSize = try container.decodeIfPresent(String.self, forKey: .junkPacketMinSize)
self.junkPacketMaxSize = try container.decodeIfPresent(String.self, forKey: .junkPacketMaxSize)
self.initPacketJunkSize = try container.decodeIfPresent(String.self, forKey: .initPacketJunkSize)
self.responsePacketJunkSize = try container.decodeIfPresent(String.self, forKey: .responsePacketJunkSize)
self.clientIP = try container.decode(String.self, forKey: .clientIP)
self.clientPrivateKey = try container.decode(String.self, forKey: .clientPrivateKey)
self.clientPublicKey = try container.decode(String.self, forKey: .clientPublicKey)
self.serverPublicKey = try container.decode(String.self, forKey: .serverPublicKey)
self.presharedKey = try container.decode(String.self, forKey: .presharedKey)
self.allowedIPs = try container.decodeIfPresent([String].self, forKey: .allowedIPs) ?? ["0.0.0.0/0", "::/0"]
self.persistentKeepAlive = try container.decodeIfPresent(String.self, forKey: .persistentKeepAlive) ?? "25"
self.hostName = try container.decode(String.self, forKey: .hostName)
self.port = try container.decode(Int.self, forKey: .port)
}
}
struct WGConfig: Decodable {
let data: WGConfigData
let configVersion: Int
let description: String
let dns1: String
let dns2: String
let hostName: String
let `protocol`: String
let splitTunnelSites: [String]
let splitTunnelType: Int
enum CodingKeys: String, CodingKey {
case awgConfigData = "awg_config_data", wgConfigData = "wireguard_config_data"
case configData
case configVersion = "config_version"
case description
case dns1
case dns2
case hostName
case `protocol`
case splitTunnelSites
case splitTunnelType
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if container.contains(.awgConfigData) {
self.data = try container.decode(WGConfigData.self, forKey: .awgConfigData)
} else {
self.data = try container.decode(WGConfigData.self, forKey: .wgConfigData)
}
self.configVersion = try container.decode(Int.self, forKey: .configVersion)
self.description = try container.decode(String.self, forKey: .description)
self.dns1 = try container.decode(String.self, forKey: .dns1)
self.dns2 = try container.decode(String.self, forKey: .dns2)
self.hostName = try container.decode(String.self, forKey: .hostName)
self.protocol = try container.decode(String.self, forKey: .protocol)
self.splitTunnelSites = try container.decode([String].self, forKey: .splitTunnelSites)
self.splitTunnelType = try container.decode(Int.self, forKey: .splitTunnelType)
}
var str: String { var str: String {
""" """
[Interface] [Interface]
Address = \(data.clientIP)/32 Address = \(clientIP)
DNS = \(dns1), \(dns2) DNS = \(dns1), \(dns2)
PrivateKey = \(data.clientPrivateKey) PrivateKey = \(clientPrivateKey)
\(data.settings) \(settings)
[Peer] [Peer]
PublicKey = \(data.serverPublicKey) PublicKey = \(serverPublicKey)
PresharedKey = \(data.presharedKey) PresharedKey = \(presharedKey)
AllowedIPs = \(data.allowedIPs.joined(separator: ", ")) AllowedIPs = \(allowedIPs.joined(separator: ", "))
Endpoint = \(data.hostName):\(data.port) Endpoint = \(hostName):\(port)
PersistentKeepalive = \(data.persistentKeepAlive) PersistentKeepalive = \(persistentKeepAlive)
"""
}
var redux: String {
"""
[Interface]
Address = \(clientIP)
DNS = \(dns1), \(dns2)
PrivateKey = ***
\(settings)
[Peer]
PublicKey = ***
PresharedKey = ***
AllowedIPs = \(allowedIPs.joined(separator: ", "))
Endpoint = \(hostName):\(port)
PersistentKeepalive = \(persistentKeepAlive)
""" """
} }
} }
struct OpenVPNConfig: Decodable {
let config: String
let splitTunnelType: Int
let splitTunnelSites: [String]
var str: String {
"splitTunnelType: \(splitTunnelType) splitTunnelSites: \(splitTunnelSites) config: \(config)"
}
}
+119 -33
View File
@@ -235,7 +235,6 @@ void IosController::checkStatus()
m_rxBytes = rxBytes; m_rxBytes = rxBytes;
m_txBytes = txBytes; m_txBytes = txBytes;
}); });
} }
void IosController::vpnStatusDidChange(void *pNotification) void IosController::vpnStatusDidChange(void *pNotification)
@@ -357,7 +356,22 @@ bool IosController::setupOpenVPN()
QJsonObject ovpn = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::OpenVpn)].toObject(); QJsonObject ovpn = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::OpenVpn)].toObject();
QString ovpnConfig = ovpn[config_key::config].toString(); QString ovpnConfig = ovpn[config_key::config].toString();
return startOpenVPN(ovpnConfig); QJsonObject openVPNConfig {};
openVPNConfig.insert(config_key::config, ovpnConfig);
openVPNConfig.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(" ");
}
openVPNConfig.insert(config_key::splitTunnelSites, splitTunnelSites);
QJsonDocument openVPNConfigDoc(openVPNConfig);
QString openVPNConfigStr(openVPNConfigDoc.toJson(QJsonDocument::Compact));
return startOpenVPN(openVPNConfigStr);
} }
bool IosController::setupCloak() bool IosController::setupCloak()
@@ -394,27 +408,123 @@ bool IosController::setupCloak()
ovpnConfig.append(cloakBase64); ovpnConfig.append(cloakBase64);
ovpnConfig.append("\n</cloak>\n"); ovpnConfig.append("\n</cloak>\n");
return startOpenVPN(ovpnConfig); QJsonObject openVPNConfig {};
openVPNConfig.insert(config_key::config, ovpnConfig);
openVPNConfig.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(" ");
}
openVPNConfig.insert(config_key::splitTunnelSites, splitTunnelSites);
QJsonDocument openVPNConfigDoc(openVPNConfig);
QString openVPNConfigStr(openVPNConfigDoc.toJson(QJsonDocument::Compact));
return startOpenVPN(openVPNConfigStr);
} }
bool IosController::setupWireGuard() bool IosController::setupWireGuard()
{ {
QJsonObject config = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::WireGuard)].toObject(); QJsonObject config = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::WireGuard)].toObject();
QJsonDocument doc(m_rawConfig); QJsonObject wgConfig {};
QString wgConfig(doc.toJson(QJsonDocument::Compact)); wgConfig.insert(config_key::dns1, m_rawConfig[config_key::dns1]);
wgConfig.insert(config_key::dns2, m_rawConfig[config_key::dns2]);
wgConfig.insert(config_key::hostName, config[config_key::hostName]);
wgConfig.insert(config_key::port, config[config_key::port]);
wgConfig.insert(config_key::client_ip, config[config_key::client_ip]);
wgConfig.insert(config_key::client_priv_key, config[config_key::client_priv_key]);
wgConfig.insert(config_key::server_pub_key, config[config_key::server_pub_key]);
wgConfig.insert(config_key::psk_key, config[config_key::psk_key]);
wgConfig.insert(config_key::splitTunnelType, m_rawConfig[config_key::splitTunnelType]);
return startWireGuard(wgConfig); QJsonArray splitTunnelSites = m_rawConfig[config_key::splitTunnelSites].toArray();
for(int index = 0; index < splitTunnelSites.count(); index++) {
splitTunnelSites[index] = splitTunnelSites[index].toString().remove(" ");
}
wgConfig.insert(config_key::splitTunnelSites, splitTunnelSites);
if (config.contains(config_key::allowed_ips)) {
QJsonArray allowed_ips;
QStringList allowed_ips_list = config[config_key::allowed_ips].toString().split(", ");
for(int index = 0; index < allowed_ips_list.length(); index++) {
allowed_ips.append(allowed_ips_list[index]);
}
wgConfig.insert(config_key::allowed_ips, allowed_ips);
} else {
QJsonArray allowed_ips { "0.0.0.0/0", "::/0" };
wgConfig.insert(config_key::allowed_ips, allowed_ips);
}
wgConfig.insert("persistent_keep_alive", "25");
QJsonDocument wgConfigDoc(wgConfig);
QString wgConfigDocStr(wgConfigDoc.toJson(QJsonDocument::Compact));
return startWireGuard(wgConfigDocStr);
} }
bool IosController::setupAwg() bool IosController::setupAwg()
{ {
QJsonObject config = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::Awg)].toObject(); QJsonObject config = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::Awg)].toObject();
QJsonDocument doc(m_rawConfig); QJsonObject wgConfig {};
QString wgConfig(doc.toJson(QJsonDocument::Compact)); wgConfig.insert(config_key::dns1, m_rawConfig[config_key::dns1]);
wgConfig.insert(config_key::dns2, m_rawConfig[config_key::dns2]);
wgConfig.insert(config_key::hostName, config[config_key::hostName]);
wgConfig.insert(config_key::port, config[config_key::port]);
wgConfig.insert(config_key::client_ip, config[config_key::client_ip]);
wgConfig.insert(config_key::client_priv_key, config[config_key::client_priv_key]);
wgConfig.insert(config_key::server_pub_key, config[config_key::server_pub_key]);
wgConfig.insert(config_key::psk_key, config[config_key::psk_key]);
wgConfig.insert(config_key::splitTunnelType, m_rawConfig[config_key::splitTunnelType]);
return startWireGuard(wgConfig); QJsonArray splitTunnelSites = m_rawConfig[config_key::splitTunnelSites].toArray();
for(int index = 0; index < splitTunnelSites.count(); index++) {
splitTunnelSites[index] = splitTunnelSites[index].toString().remove(" ");
}
wgConfig.insert(config_key::splitTunnelSites, splitTunnelSites);
if (config.contains(config_key::allowed_ips)) {
QJsonArray allowed_ips;
QStringList allowed_ips_list = config[config_key::allowed_ips].toString().split(", ");
for(int index = 0; index < allowed_ips_list.length(); index++) {
allowed_ips.append(allowed_ips_list[index]);
}
wgConfig.insert(config_key::allowed_ips, allowed_ips);
} else {
QJsonArray allowed_ips { "0.0.0.0/0", "::/0" };
wgConfig.insert(config_key::allowed_ips, allowed_ips);
}
wgConfig.insert("persistent_keep_alive", "25");
wgConfig.insert(config_key::initPacketMagicHeader, config[config_key::initPacketMagicHeader]);
wgConfig.insert(config_key::responsePacketMagicHeader, config[config_key::responsePacketMagicHeader]);
wgConfig.insert(config_key::underloadPacketMagicHeader, config[config_key::underloadPacketMagicHeader]);
wgConfig.insert(config_key::transportPacketMagicHeader, config[config_key::transportPacketMagicHeader]);
wgConfig.insert(config_key::initPacketJunkSize, config[config_key::initPacketJunkSize]);
wgConfig.insert(config_key::responsePacketJunkSize, config[config_key::responsePacketJunkSize]);
wgConfig.insert(config_key::junkPacketCount, config[config_key::junkPacketCount]);
wgConfig.insert(config_key::junkPacketMinSize, config[config_key::junkPacketMinSize]);
wgConfig.insert(config_key::junkPacketMaxSize, config[config_key::junkPacketMaxSize]);
QJsonDocument wgConfigDoc(wgConfig);
QString wgConfigDocStr(wgConfigDoc.toJson(QJsonDocument::Compact));
return startWireGuard(wgConfigDocStr);
} }
bool IosController::startOpenVPN(const QString &config) bool IosController::startOpenVPN(const QString &config)
@@ -459,12 +569,6 @@ void IosController::startTunnel()
m_rxBytes = 0; m_rxBytes = 0;
m_txBytes = 0; m_txBytes = 0;
int STT = m_rawConfig["splitTunnelType"].toInt();
QJsonArray splitTunnelSites = m_rawConfig["splitTunnelSites"].toArray();
QJsonDocument doc;
doc.setArray(splitTunnelSites);
QString STS(doc.toJson());
[m_currentTunnel setEnabled:YES]; [m_currentTunnel setEnabled:YES];
[m_currentTunnel saveToPreferencesWithCompletionHandler:^(NSError *saveError) { [m_currentTunnel saveToPreferencesWithCompletionHandler:^(NSError *saveError) {
@@ -485,23 +589,6 @@ void IosController::startTunnel()
NSError *startError = nil; NSError *startError = nil;
qDebug() << iosStatusToState(m_currentTunnel.connection.status); qDebug() << iosStatusToState(m_currentTunnel.connection.status);
NSString *actionKey = [NSString stringWithUTF8String:MessageKey::action];
NSString *actionValue = [NSString stringWithUTF8String:Action::start];
NSString *tunnelIdKey = [NSString stringWithUTF8String:MessageKey::tunnelId];
NSString *tunnelIdValue = !m_tunnelId.isEmpty() ? m_tunnelId.toNSString() : @"";
NSString *SplitTunnelTypeKey = [NSString stringWithUTF8String:MessageKey::SplitTunnelType];
NSString *SplitTunnelTypeValue = [NSString stringWithFormat:@"%d",STT];
NSString *SplitTunnelSitesKey = [NSString stringWithUTF8String:MessageKey::SplitTunnelSites];
NSString *SplitTunnelSitesValue = STS.toNSString();
NSDictionary* message = @{actionKey: actionValue, tunnelIdKey: tunnelIdValue,
SplitTunnelTypeKey: SplitTunnelTypeValue, SplitTunnelSitesKey: SplitTunnelSitesValue};
sendVpnExtensionMessage(message);
BOOL started = [m_currentTunnel.connection startVPNTunnelWithOptions:nil andReturnError:&startError]; BOOL started = [m_currentTunnel.connection startVPNTunnelWithOptions:nil andReturnError:&startError];
if (!started || startError) { if (!started || startError) {
@@ -516,7 +603,6 @@ void IosController::startTunnel()
}]; }];
} }
bool IosController::isOurManager(NETunnelProviderManager* manager) { bool IosController::isOurManager(NETunnelProviderManager* manager) {
NETunnelProviderProtocol* tunnelProto = (NETunnelProviderProtocol*)manager.protocolConfiguration; NETunnelProviderProtocol* tunnelProto = (NETunnelProviderProtocol*)manager.protocolConfiguration;
@@ -21,7 +21,7 @@
} }
- (void) vpnConfigurationDidChange:(NSNotification *)notification { - (void) vpnConfigurationDidChange:(NSNotification *)notification {
cppController->vpnStatusDidChange(notification); // cppController->vpnStatusDidChange(notification);
} }
+1
View File
@@ -20,6 +20,7 @@ namespace amnezia
constexpr char dns1[] = "dns1"; constexpr char dns1[] = "dns1";
constexpr char dns2[] = "dns2"; constexpr char dns2[] = "dns2";
constexpr char serverIndex[] = "serverIndex";
constexpr char description[] = "description"; constexpr char description[] = "description";
constexpr char name[] = "name"; constexpr char name[] = "name";
constexpr char cert[] = "cert"; constexpr char cert[] = "cert";
+1 -1
View File
@@ -1 +1 @@
sudo docker build --no-cache --pull -t $CONTAINER_NAME $DOCKERFILE_FOLDER --build-arg SERVER_ARCH=$(uname -m) sudo docker build --no-cache --pull -t $CONTAINER_NAME $DOCKERFILE_FOLDER
+13 -15
View File
@@ -1,9 +1,8 @@
FROM alpine:3.15 FROM alpine:3.15
LABEL maintainer="AmneziaVPN" LABEL maintainer="AmneziaVPN"
ARG SS_RELEASE="v1.13.1" ARG SS_RELEASE="v1.18.1"
ARG CLOAK_RELEASE="v2.5.5" ARG CLOAK_RELEASE="v2.8.0"
ARG SERVER_ARCH
#Install required packages #Install required packages
RUN apk add --no-cache curl openvpn easy-rsa bash netcat-openbsd dumb-init rng-tools RUN apk add --no-cache curl openvpn easy-rsa bash netcat-openbsd dumb-init rng-tools
@@ -16,18 +15,17 @@ RUN mkdir -p /opt/amnezia
RUN echo -e "#!/bin/bash\ntail -f /dev/null" > /opt/amnezia/start.sh RUN echo -e "#!/bin/bash\ntail -f /dev/null" > /opt/amnezia/start.sh
RUN chmod a+x /opt/amnezia/start.sh RUN chmod a+x /opt/amnezia/start.sh
RUN if [ $SERVER_ARCH="x86_64" ]; then CK_ARCH="amd64"; \ RUN SERVER_ARCH=$(uname -m) && \
elif [ $SERVER_ARCH="i686" ]; then CK_ARCH="386"; \ if [ $SERVER_ARCH="x86_64" ]; then CK_ARCH="amd64"; \
elif [ $SERVER_ARCH="aarch64" ]; then CK_ARCH="arm64"; \ elif [ $SERVER_ARCH="i686" ]; then CK_ARCH="386"; \
elif [ $SERVER_ARCH="arm" ]; then CK_ARCH="arm"; \ elif [ $SERVER_ARCH="aarch64" ]; then CK_ARCH="arm64"; \
else exit -1; fi && \ elif [ $SERVER_ARCH="arm" ]; then CK_ARCH="arm"; \
curl -L https://github.com/cbeuw/Cloak/releases/download/${CLOAK_RELEASE}/ck-server-linux-${CK_ARCH}-${CLOAK_RELEASE} > /usr/bin/ck-server else exit -1; fi && \
RUN chmod a+x /usr/bin/ck-server curl -L https://github.com/cbeuw/Cloak/releases/download/${CLOAK_RELEASE}/ck-server-linux-${CK_ARCH}-${CLOAK_RELEASE} > /usr/bin/ck-server && \
chmod a+x /usr/bin/ck-server && \
RUN curl -L https://github.com/shadowsocks/shadowsocks-rust/releases/download/${SS_RELEASE}/shadowsocks-${SS_RELEASE}.${SERVER_ARCH}-unknown-linux-musl.tar.xz > /usr/bin/ss.tar.xz curl -L https://github.com/shadowsocks/shadowsocks-rust/releases/download/${SS_RELEASE}/shadowsocks-${SS_RELEASE}.${SERVER_ARCH}-unknown-linux-musl.tar.xz > /usr/bin/ss.tar.xz && \
tar -Jxvf /usr/bin/ss.tar.xz -C /usr/bin/ && \
RUN tar -Jxvf /usr/bin/ss.tar.xz -C /usr/bin/ chmod a+x /usr/bin/ssserver
RUN chmod a+x /usr/bin/ssserver
# Tune network # Tune network
RUN echo -e " \n\ RUN echo -e " \n\
@@ -1,8 +1,7 @@
FROM alpine:3.15 FROM alpine:3.15
LABEL maintainer="AmneziaVPN" LABEL maintainer="AmneziaVPN"
ARG SS_RELEASE="v1.13.1" ARG SS_RELEASE="v1.18.1"
ARG SERVER_ARCH
#Install required packages #Install required packages
RUN apk add --no-cache curl openvpn easy-rsa bash netcat-openbsd dumb-init rng-tools xz RUN apk add --no-cache curl openvpn easy-rsa bash netcat-openbsd dumb-init rng-tools xz
@@ -15,7 +14,16 @@ RUN mkdir -p /opt/amnezia
RUN echo -e "#!/bin/bash\ntail -f /dev/null" > /opt/amnezia/start.sh RUN echo -e "#!/bin/bash\ntail -f /dev/null" > /opt/amnezia/start.sh
RUN chmod a+x /opt/amnezia/start.sh RUN chmod a+x /opt/amnezia/start.sh
RUN curl -L https://github.com/shadowsocks/shadowsocks-rust/releases/download/${SS_RELEASE}/shadowsocks-${SS_RELEASE}.${SERVER_ARCH}-unknown-linux-musl.tar.xz > /usr/bin/ss.tar.xz;\ RUN SERVER_ARCH=$(uname -m); \
SUFFIX=""; \
if [ ! -z "$(echo ${SERVER_ARCH} | grep -i arm)" ]; then \
if [ ! -z "$(cat /proc/cpuinfo | grep -i vfp)" ]; then \
SUFFIX="eabihf"; \
else \
SUFFIX="eabi"; \
fi; \
fi; \
curl -L https://github.com/shadowsocks/shadowsocks-rust/releases/download/${SS_RELEASE}/shadowsocks-${SS_RELEASE}.${SERVER_ARCH}-unknown-linux-musl${SUFFIX}.tar.xz > /usr/bin/ss.tar.xz;\
tar -Jxvf /usr/bin/ss.tar.xz -C /usr/bin/;\ tar -Jxvf /usr/bin/ss.tar.xz -C /usr/bin/;\
chmod a+x /usr/bin/ssserver; chmod a+x /usr/bin/ssserver;
@@ -1,4 +1,5 @@
sudo docker ps -a | grep amnezia | awk '{print $1}' | xargs sudo docker stop sudo docker ps -a | grep amnezia | awk '{print $1}' | xargs sudo docker stop;\
sudo docker ps -a | grep amnezia | awk '{print $1}' | xargs sudo docker rm -fv sudo docker ps -a | grep amnezia | awk '{print $1}' | xargs sudo docker rm -fv;\
sudo docker images -a | grep amnezia | awk '{print $3}' | xargs sudo docker rmi sudo docker images -a | grep amnezia | awk '{print $3}' | xargs sudo docker rmi;\
sudo docker network ls | grep amnezia-dns-net | awk '{print $1}' | xargs sudo docker network rm sudo docker network ls | grep amnezia-dns-net | awk '{print $1}' | xargs sudo docker network rm;\
sudo rm -frd /opt/amnezia
+2 -2
View File
@@ -1,3 +1,3 @@
sudo docker stop $CONTAINER_NAME sudo docker stop $CONTAINER_NAME;\
sudo docker rm -fv $CONTAINER_NAME sudo docker rm -fv $CONTAINER_NAME;\
sudo docker rmi $CONTAINER_NAME sudo docker rmi $CONTAINER_NAME
+2
View File
@@ -68,6 +68,7 @@ void Settings::removeServer(int index)
servers.removeAt(index); servers.removeAt(index);
setServersArray(servers); setServersArray(servers);
emit serverRemoved(index);
} }
bool Settings::editServer(int index, const QJsonObject &server) bool Settings::editServer(int index, const QJsonObject &server)
@@ -338,6 +339,7 @@ QString Settings::secondaryDns() const
void Settings::clearSettings() void Settings::clearSettings()
{ {
m_settings.clearSettings(); m_settings.clearSettings();
emit settingsCleared();
} }
ServerCredentials Settings::defaultServerCredentials() const ServerCredentials Settings::defaultServerCredentials() const
+2
View File
@@ -191,6 +191,8 @@ public:
signals: signals:
void saveLogsChanged(bool enabled); void saveLogsChanged(bool enabled);
void serverRemoved(int serverIndex);
void settingsCleared();
private: private:
QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const; QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -25,12 +25,13 @@ ConnectionController::ConnectionController(const QSharedPointer<ServersModel> &s
void ConnectionController::openConnection() void ConnectionController::openConnection()
{ {
if (!m_containersModel->isAnyContainerInstalled()) { int serverIndex = m_serversModel->getDefaultServerIndex();
if (!m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) {
emit noInstalledContainers(); emit noInstalledContainers();
return; return;
} }
int serverIndex = m_serversModel->getDefaultServerIndex();
ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex); ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex);
DockerContainer container = qvariant_cast<DockerContainer>(m_serversModel->data(serverIndex, ServersModel::Roles::DefaultContainerRole)); DockerContainer container = qvariant_cast<DockerContainer>(m_serversModel->data(serverIndex, ServersModel::Roles::DefaultContainerRole));
@@ -418,4 +418,6 @@ void ExportController::clearPreviousConfig()
m_config.clear(); m_config.clear();
m_nativeConfigString.clear(); m_nativeConfigString.clear();
m_qrCodes.clear(); m_qrCodes.clear();
emit exportConfigChanged();
} }
+23 -2
View File
@@ -77,7 +77,16 @@ void PageController::closeWindow()
void PageController::keyPressEvent(Qt::Key key) void PageController::keyPressEvent(Qt::Key key)
{ {
switch (key) { switch (key) {
case Qt::Key_Back: emit closePage(); case Qt::Key_Back:
case Qt::Key_Escape: {
if (m_drawerDepth) {
emit closeTopDrawer();
setDrawerDepth(getDrawerDepth() - 1);
} else {
emit escapePressed();
}
break;
}
default: return; default: return;
} }
} }
@@ -123,7 +132,7 @@ bool PageController::isTriggeredByConnectButton()
return m_isTriggeredByConnectButton; return m_isTriggeredByConnectButton;
} }
void PageController::setTriggeredBtConnectButton(bool trigger) void PageController::setTriggeredByConnectButton(bool trigger)
{ {
m_isTriggeredByConnectButton = trigger; m_isTriggeredByConnectButton = trigger;
} }
@@ -132,3 +141,15 @@ void PageController::closeApplication()
{ {
qApp->quit(); qApp->quit();
} }
void PageController::setDrawerDepth(const int depth)
{
if (depth >= 0) {
m_drawerDepth = depth;
}
}
int PageController::getDrawerDepth()
{
return m_drawerDepth;
}
+10 -2
View File
@@ -83,10 +83,13 @@ public slots:
void showOnStartup(); void showOnStartup();
bool isTriggeredByConnectButton(); bool isTriggeredByConnectButton();
void setTriggeredBtConnectButton(bool trigger); void setTriggeredByConnectButton(bool trigger);
void closeApplication(); void closeApplication();
void setDrawerDepth(const int depth);
int getDrawerDepth();
signals: signals:
void goToPage(PageLoader::PageEnum page, bool slide = true); void goToPage(PageLoader::PageEnum page, bool slide = true);
void goToStartPage(); void goToStartPage();
@@ -105,7 +108,7 @@ signals:
void showNotificationMessage(const QString &message); void showNotificationMessage(const QString &message);
void showBusyIndicator(bool visible); void showBusyIndicator(bool visible);
void enableTabBar(bool enabled); void disableControls(bool disabled);
void hideMainWindow(); void hideMainWindow();
void raiseMainWindow(); void raiseMainWindow();
@@ -113,12 +116,17 @@ signals:
void showPassphraseRequestDrawer(); void showPassphraseRequestDrawer();
void passphraseRequestDrawerClosed(QString passphrase); void passphraseRequestDrawerClosed(QString passphrase);
void escapePressed();
void closeTopDrawer();
private: private:
QSharedPointer<ServersModel> m_serversModel; QSharedPointer<ServersModel> m_serversModel;
std::shared_ptr<Settings> m_settings; std::shared_ptr<Settings> m_settings;
bool m_isTriggeredByConnectButton; bool m_isTriggeredByConnectButton;
int m_drawerDepth = 0;
}; };
#endif // PAGECONTROLLER_H #endif // PAGECONTROLLER_H
@@ -152,7 +152,12 @@ void SettingsController::clearSettings()
m_languageModel->changeLanguage( m_languageModel->changeLanguage(
static_cast<LanguageSettings::AvailableLanguageEnum>(m_languageModel->getCurrentLanguageIndex())); static_cast<LanguageSettings::AvailableLanguageEnum>(m_languageModel->getCurrentLanguageIndex()));
m_sitesModel->setRouteMode(Settings::RouteMode::VpnAllSites); m_sitesModel->setRouteMode(Settings::RouteMode::VpnAllSites);
emit changeSettingsFinished(tr("All settings have been reset to default values")); emit changeSettingsFinished(tr("All settings have been reset to default values"));
#ifdef Q_OS_IOS
AmneziaVPN::clearSettings();
#endif
} }
void SettingsController::clearCachedProfiles() void SettingsController::clearCachedProfiles()
+4 -4
View File
@@ -92,11 +92,11 @@ QString SystemController::getFileName(const QString &acceptLabel, const QString
mainFileDialog->setProperty("acceptLabel", QVariant::fromValue(acceptLabel)); mainFileDialog->setProperty("acceptLabel", QVariant::fromValue(acceptLabel));
mainFileDialog->setProperty("nameFilters", QVariant::fromValue(QStringList(nameFilter))); mainFileDialog->setProperty("nameFilters", QVariant::fromValue(QStringList(nameFilter)));
if (!selectedFile.isEmpty()) {
mainFileDialog->setProperty("selectedFile", QVariant::fromValue(selectedFile));
}
mainFileDialog->setProperty("isSaveMode", QVariant::fromValue(isSaveMode));
mainFileDialog->setProperty("defaultSuffix", QVariant::fromValue(defaultSuffix)); mainFileDialog->setProperty("defaultSuffix", QVariant::fromValue(defaultSuffix));
mainFileDialog->setProperty("isSaveMode", QVariant::fromValue(isSaveMode));
if (!selectedFile.isEmpty()) {
mainFileDialog->setProperty("selectedFile", QVariant::fromValue(QUrl(selectedFile)));
}
QMetaObject::invokeMethod(mainFileDialog, "open"); QMetaObject::invokeMethod(mainFileDialog, "open");
bool isFileDialogAccepted = false; bool isFileDialogAccepted = false;
-14
View File
@@ -83,20 +83,6 @@ QJsonObject ContainersModel::getContainerConfig(const int containerIndex)
return qvariant_cast<QJsonObject>(data(index(containerIndex), ConfigRole)); return qvariant_cast<QJsonObject>(data(index(containerIndex), ConfigRole));
} }
bool ContainersModel::isAnyContainerInstalled()
{
for (int row=0; row < rowCount(); row++) {
QModelIndex idx = this->index(row, 0);
if (this->data(idx, IsInstalledRole).toBool() &&
this->data(idx, ServiceTypeRole).toInt() == ServiceType::Vpn) {
return true;
}
}
return false;
}
QHash<int, QByteArray> ContainersModel::roleNames() const QHash<int, QByteArray> ContainersModel::roleNames() const
{ {
QHash<int, QByteArray> roles; QHash<int, QByteArray> roles;
-2
View File
@@ -49,8 +49,6 @@ public slots:
QJsonObject getContainerConfig(const int containerIndex); QJsonObject getContainerConfig(const int containerIndex);
bool isAnyContainerInstalled();
protected: protected:
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> roleNames() const override;
+17 -10
View File
@@ -87,6 +87,9 @@ QVariant ServersModel::data(const QModelIndex &index, int role) const
case DefaultContainerRole: { case DefaultContainerRole: {
return ContainerProps::containerFromString(server.value(config_key::defaultContainer).toString()); return ContainerProps::containerFromString(server.value(config_key::defaultContainer).toString());
} }
case HasInstalledContainers: {
return serverHasInstalledContainers(index.row());
}
case IsServerFromApiRole: { case IsServerFromApiRole: {
return server.value(config_key::configVersion).toInt(); return server.value(config_key::configVersion).toInt();
} }
@@ -302,6 +305,7 @@ QHash<int, QByteArray> ServersModel::roleNames() const
roles[ContainsAmneziaDnsRole] = "containsAmneziaDns"; roles[ContainsAmneziaDnsRole] = "containsAmneziaDns";
roles[DefaultContainerRole] = "defaultContainer"; roles[DefaultContainerRole] = "defaultContainer";
roles[HasInstalledContainers] = "hasInstalledContainers";
roles[IsServerFromApiRole] = "isServerFromApi"; roles[IsServerFromApiRole] = "isServerFromApi";
return roles; return roles;
@@ -548,6 +552,19 @@ bool ServersModel::isServerFromApiAlreadyExists(const quint16 crc)
return false; return false;
} }
bool ServersModel::serverHasInstalledContainers(const int serverIndex) const
{
QJsonObject server = m_servers.at(serverIndex).toObject();
const auto containers = server.value(config_key::containers).toArray();
for (auto it = containers.begin(); it != containers.end(); it++) {
auto container = ContainerProps::containerFromString(it->toObject().value(config_key::container).toString());
if (ContainerProps::containerService(container) == ServiceType::Vpn) {
return true;
}
}
return false;
}
QVariant ServersModel::getDefaultServerData(const QString roleString) QVariant ServersModel::getDefaultServerData(const QString roleString)
{ {
auto roles = roleNames(); auto roles = roleNames();
@@ -560,11 +577,6 @@ QVariant ServersModel::getDefaultServerData(const QString roleString)
return {}; return {};
} }
void ServersModel::setDefaultServerData(const QString roleString, const QVariant &value)
{
}
QVariant ServersModel::getProcessedServerData(const QString roleString) QVariant ServersModel::getProcessedServerData(const QString roleString)
{ {
auto roles = roleNames(); auto roles = roleNames();
@@ -577,11 +589,6 @@ QVariant ServersModel::getProcessedServerData(const QString roleString)
return {}; return {};
} }
void ServersModel::setProcessedServerData(const QString roleString, const QVariant &value)
{
}
bool ServersModel::isDefaultServerDefaultContainerHasSplitTunneling() bool ServersModel::isDefaultServerDefaultContainerHasSplitTunneling()
{ {
auto server = m_servers.at(m_defaultServerIndex).toObject(); auto server = m_servers.at(m_defaultServerIndex).toObject();
+4 -2
View File
@@ -28,6 +28,7 @@ public:
DefaultContainerRole, DefaultContainerRole,
HasInstalledContainers,
IsServerFromApiRole, IsServerFromApiRole,
HasAmneziaDns HasAmneziaDns
@@ -101,10 +102,8 @@ public slots:
bool isServerFromApiAlreadyExists(const quint16 crc); bool isServerFromApiAlreadyExists(const quint16 crc);
QVariant getDefaultServerData(const QString roleString); QVariant getDefaultServerData(const QString roleString);
void setDefaultServerData(const QString roleString, const QVariant &value);
QVariant getProcessedServerData(const QString roleString); QVariant getProcessedServerData(const QString roleString);
void setProcessedServerData(const QString roleString, const QVariant &value);
bool isDefaultServerDefaultContainerHasSplitTunneling(); bool isDefaultServerDefaultContainerHasSplitTunneling();
@@ -123,6 +122,7 @@ signals:
private: private:
ServerCredentials serverCredentials(int index) const; ServerCredentials serverCredentials(int index) const;
void updateContainersModel(); void updateContainersModel();
void updateDefaultServerContainersModel(); void updateDefaultServerContainersModel();
@@ -130,6 +130,8 @@ private:
bool isAmneziaDnsContainerInstalled(const int serverIndex) const; bool isAmneziaDnsContainerInstalled(const int serverIndex) const;
bool serverHasInstalledContainers(const int serverIndex) const;
QJsonArray m_servers; QJsonArray m_servers;
std::shared_ptr<Settings> m_settings; std::shared_ptr<Settings> m_settings;
@@ -38,6 +38,14 @@ DrawerType2 {
expandedContent: Item { expandedContent: Item {
implicitHeight: root.expandedHeight implicitHeight: root.expandedHeight
Connections {
target: root
function onOpened() {
header.forceActiveFocus()
}
}
Header2Type { Header2Type {
id: header id: header
anchors.top: parent.top anchors.top: parent.top
@@ -48,6 +56,8 @@ DrawerType2 {
anchors.rightMargin: 16 anchors.rightMargin: 16
headerText: root.headerText headerText: root.headerText
KeyNavigation.tab: shareButton
} }
FlickableType { FlickableType {
@@ -68,12 +78,15 @@ DrawerType2 {
visible: root.contentVisible visible: root.contentVisible
BasicButtonType { BasicButtonType {
id: shareButton
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 16 Layout.topMargin: 16
text: qsTr("Share") text: qsTr("Share")
imageSource: "qrc:/images/controls/share-2.svg" imageSource: "qrc:/images/controls/share-2.svg"
KeyNavigation.tab: copyConfigTextButton
clickedFunc: function() { clickedFunc: function() {
var fileName = "" var fileName = ""
if (GC.isMobile()) { if (GC.isMobile()) {
@@ -107,6 +120,8 @@ DrawerType2 {
text: qsTr("Copy") text: qsTr("Copy")
imageSource: "qrc:/images/controls/copy.svg" imageSource: "qrc:/images/controls/copy.svg"
KeyNavigation.tab: copyNativeConfigStringButton.visible ? copyNativeConfigStringButton : showSettingsButton
} }
BasicButtonType { BasicButtonType {
@@ -125,9 +140,13 @@ DrawerType2 {
text: qsTr("Copy config string") text: qsTr("Copy config string")
imageSource: "qrc:/images/controls/copy.svg" imageSource: "qrc:/images/controls/copy.svg"
KeyNavigation.tab: showSettingsButton
} }
BasicButtonType { BasicButtonType {
id: showSettingsButton
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 24 Layout.topMargin: 24
@@ -143,6 +162,8 @@ DrawerType2 {
clickedFunc: function() { clickedFunc: function() {
configContentDrawer.open() configContentDrawer.open()
} }
KeyNavigation.tab: header
} }
DrawerType2 { DrawerType2 {
@@ -258,6 +279,8 @@ DrawerType2 {
} }
Rectangle { Rectangle {
id: qrCodeContainer
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: width Layout.preferredHeight: width
Layout.topMargin: 20 Layout.topMargin: 20
+9 -8
View File
@@ -33,22 +33,23 @@ Button {
hoverEnabled: true hoverEnabled: true
background: Rectangle { background: Rectangle {
id: background_border id: focusBorder
color: "transparent" color: "transparent"
border.color: root.activeFocus ? root.borderFocusedColor : "transparent" border.color: root.activeFocus ? root.borderFocusedColor : "transparent"
border.width: root.activeFocus ? root.borderFocusedWidth : "transparent" border.width: root.activeFocus ? root.borderFocusedWidth : "transparent"
anchors.fill: parent anchors.fill: parent
radius: 16 radius: 16
Rectangle { Rectangle {
id: background id: background
anchors.fill: background_border anchors.fill: focusBorder
anchors.margins: root.activeFocus ? 2: 0 anchors.margins: root.activeFocus ? 2 : 0
radius: 16 radius: root.activeFocus ? 14 : 16
color: { color: {
if (root.enabled) { if (root.enabled) {
if (root.pressed) { if (root.pressed) {
@@ -59,8 +60,8 @@ Button {
return disabledColor return disabledColor
} }
} }
border.color: root.activeFocus ? "transparent" : borderColor border.color: borderColor
border.width: root.activeFocus ? 0 : borderWidth border.width: borderWidth
Behavior on color { Behavior on color {
PropertyAnimation { duration: 200 } PropertyAnimation { duration: 200 }
@@ -95,13 +96,13 @@ Button {
} }
MouseArea { MouseArea {
anchors.fill: background_border anchors.fill: focusBorder
enabled: false enabled: false
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
} }
contentItem: Item { contentItem: Item {
anchors.fill: background_border anchors.fill: focusBorder
implicitWidth: content.implicitWidth implicitWidth: content.implicitWidth
implicitHeight: content.implicitHeight implicitHeight: content.implicitHeight
+24
View File
@@ -25,6 +25,8 @@ Item {
property real expandedHeight property real expandedHeight
property real collapsedHeight: 0 property real collapsedHeight: 0
property int depthIndex: 0
signal entered signal entered
signal exited signal exited
signal pressed(bool pressed, bool entered) signal pressed(bool pressed, bool entered)
@@ -36,6 +38,24 @@ Item {
signal closed signal closed
signal opened signal opened
Connections {
target: PageController
function onCloseTopDrawer() {
if (depthIndex === PageController.getDrawerDepth()) {
if (isCollapsed) {
return
}
aboutToHide()
drawerContent.state = root.drawerCollapsed
depthIndex = 0
closed()
}
}
}
Connections { Connections {
target: root target: root
@@ -47,6 +67,8 @@ Item {
aboutToHide() aboutToHide()
drawerContent.state = root.drawerCollapsed drawerContent.state = root.drawerCollapsed
depthIndex = 0
PageController.setDrawerDepth(PageController.getDrawerDepth() - 1)
closed() closed()
} }
@@ -58,6 +80,8 @@ Item {
aboutToShow() aboutToShow()
drawerContent.state = root.drawerExpanded drawerContent.state = root.drawerExpanded
depthIndex = PageController.getDrawerDepth() + 1
PageController.setDrawerDepth(depthIndex)
opened() opened()
} }
} }
+2 -2
View File
@@ -14,8 +14,8 @@ import "../Config"
PageType { PageType {
id: root id: root
Component.onCompleted: PageController.enableTabBar(false) Component.onCompleted: PageController.disableControls(true)
Component.onDestruction: PageController.enableTabBar(true) Component.onDestruction: PageController.disableControls(false)
SortFilterProxyModel { SortFilterProxyModel {
id: proxyServersModel id: proxyServersModel
+2 -2
View File
@@ -162,10 +162,10 @@ PageType {
} }
} }
expandedContent: Item { expandedContent: Item {
id: serverMenuContainer id: serverMenuContainer
implicitHeight: root.height * 0.9 implicitHeight: Qt.platform.os !== "ios" ? root.height * 0.9 : screen.height * 0.77
Component.onCompleted: { Component.onCompleted: {
drawer.expandedHeight = serverMenuContainer.implicitHeight drawer.expandedHeight = serverMenuContainer.implicitHeight
@@ -111,7 +111,7 @@ PageType {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 16 Layout.topMargin: 16
headerText: qsTr("Junk packet count") headerText: "Jc - Junk packet count"
textFieldText: junkPacketCount textFieldText: junkPacketCount
textField.validator: IntValidator { bottom: 0 } textField.validator: IntValidator { bottom: 0 }
@@ -136,7 +136,7 @@ PageType {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 16 Layout.topMargin: 16
headerText: qsTr("Junk packet minimum size") headerText: "Jmin - Junk packet minimum size"
textFieldText: junkPacketMinSize textFieldText: junkPacketMinSize
textField.validator: IntValidator { bottom: 0 } textField.validator: IntValidator { bottom: 0 }
@@ -156,7 +156,7 @@ PageType {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 16 Layout.topMargin: 16
headerText: qsTr("Junk packet maximum size") headerText: "Jmax - Junk packet maximum size"
textFieldText: junkPacketMaxSize textFieldText: junkPacketMaxSize
textField.validator: IntValidator { bottom: 0 } textField.validator: IntValidator { bottom: 0 }
@@ -176,7 +176,7 @@ PageType {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 16 Layout.topMargin: 16
headerText: qsTr("Init packet junk size") headerText: "S1 - Init packet junk size"
textFieldText: initPacketJunkSize textFieldText: initPacketJunkSize
textField.validator: IntValidator { bottom: 0 } textField.validator: IntValidator { bottom: 0 }
@@ -196,7 +196,7 @@ PageType {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 16 Layout.topMargin: 16
headerText: qsTr("Response packet junk size") headerText: "S2 - Response packet junk size"
textFieldText: responsePacketJunkSize textFieldText: responsePacketJunkSize
textField.validator: IntValidator { bottom: 0 } textField.validator: IntValidator { bottom: 0 }
@@ -216,7 +216,7 @@ PageType {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 16 Layout.topMargin: 16
headerText: qsTr("Init packet magic header") headerText: "H1 - Init packet magic header"
textFieldText: initPacketMagicHeader textFieldText: initPacketMagicHeader
textField.validator: IntValidator { bottom: 0 } textField.validator: IntValidator { bottom: 0 }
@@ -236,7 +236,7 @@ PageType {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 16 Layout.topMargin: 16
headerText: qsTr("Response packet magic header") headerText: "H2 - Response packet magic header"
textFieldText: responsePacketMagicHeader textFieldText: responsePacketMagicHeader
textField.validator: IntValidator { bottom: 0 } textField.validator: IntValidator { bottom: 0 }
@@ -256,7 +256,7 @@ PageType {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 16 Layout.topMargin: 16
headerText: qsTr("Transport packet magic header") headerText: "H4 - Transport packet magic header"
textFieldText: transportPacketMagicHeader textFieldText: transportPacketMagicHeader
textField.validator: IntValidator { bottom: 0 } textField.validator: IntValidator { bottom: 0 }
@@ -276,7 +276,7 @@ PageType {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 16 Layout.topMargin: 16
headerText: qsTr("Underload packet magic header") headerText: "H3 - Underload packet magic header"
textFieldText: underloadPacketMagicHeader textFieldText: underloadPacketMagicHeader
textField.validator: IntValidator { bottom: 0 } textField.validator: IntValidator { bottom: 0 }
@@ -130,7 +130,7 @@ PageType {
} }
Component.onCompleted: { Component.onCompleted: {
if (header.itemAt(0)) { if (header.itemAt(0) && !GC.isMobile()) {
defaultActiveFocusItem = serverName.textField defaultActiveFocusItem = serverName.textField
} }
} }
+2 -3
View File
@@ -187,9 +187,8 @@ PageType {
visible: { visible: {
if (PageController.isTriggeredByConnectButton()) { if (PageController.isTriggeredByConnectButton()) {
PageController.setTriggeredBtConnectButton(false) PageController.setTriggeredByConnectButton(false)
return false
return ContainersModel.isAnyContainerInstalled()
} }
return true return true
@@ -14,8 +14,8 @@ import "../Config"
PageType { PageType {
id: root id: root
Component.onCompleted: PageController.enableTabBar(false) Component.onCompleted: PageController.disableControls(true)
Component.onDestruction: PageController.enableTabBar(true) Component.onDestruction: PageController.disableControls(false)
property bool isTimerRunning: true property bool isTimerRunning: true
property string progressBarText: qsTr("Usually it takes no more than 5 minutes") property string progressBarText: qsTr("Usually it takes no more than 5 minutes")
@@ -13,6 +13,8 @@ import "../Components"
PageType { PageType {
id: root id: root
property bool isControlsDisabled: false
Connections { Connections {
target: PageController target: PageController
@@ -45,6 +47,18 @@ PageType {
stackView.pop() stackView.pop()
} }
} }
function onDisableControls(disabled) {
isControlsDisabled = disabled
}
function onEscapePressed() {
if (isControlsDisabled || busyIndicator.visible) {
return
}
PageController.closePage()
}
} }
Connections { Connections {
+11 -2
View File
@@ -308,6 +308,10 @@ PageType {
ValueFilter { ValueFilter {
roleName: "hasWriteAccess" roleName: "hasWriteAccess"
value: true value: true
},
ValueFilter {
roleName: "hasInstalledContainers"
value: true
} }
] ]
} }
@@ -324,8 +328,12 @@ PageType {
} }
Component.onCompleted: { Component.onCompleted: {
serverSelectorListView.currentIndex = ServersModel.isDefaultServerHasWriteAccess() ? if (ServersModel.isDefaultServerHasWriteAccess() && ServersModel.getDefaultServerData("hasInstalledContainers")) {
proxyServersModel.mapFromSource(ServersModel.defaultIndex) : 0 serverSelectorListView.currentIndex = proxyServersModel.mapFromSource(ServersModel.defaultIndex)
} else {
serverSelectorListView.currentIndex = 0
}
serverSelectorListView.triggerCurrentItem() serverSelectorListView.triggerCurrentItem()
} }
@@ -480,6 +488,7 @@ PageType {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 40 Layout.topMargin: 40
Layout.bottomMargin: 32
enabled: shareButtonEnabled enabled: shareButtonEnabled
visible: accessTypeSelector.currentIndex === 0 visible: accessTypeSelector.currentIndex === 0
+20 -12
View File
@@ -38,14 +38,11 @@ PageType {
tabBar.enabled = !visible tabBar.enabled = !visible
} }
function onEnableTabBar(enabled) { function onDisableControls(disabled) {
tabBar.enabled = enabled tabBar.enabled = !disabled
} }
function onClosePage() { function onClosePage() {
tabBar.isServerInfoShow = tabBarStackView.currentItem.objectName !== PageController.getPagePath(PageEnum.PageSettingsServerInfo)
&& tabBarStackView.currentItem.objectName !== PageController.getPagePath(PageEnum.PageSettingsSplitTunneling)
if (tabBarStackView.depth <= 1) { if (tabBarStackView.depth <= 1) {
return return
} }
@@ -60,8 +57,6 @@ PageType {
} else { } else {
tabBarStackView.push(pagePath, { "objectName" : pagePath }, StackView.Immediate) tabBarStackView.push(pagePath, { "objectName" : pagePath }, StackView.Immediate)
} }
tabBar.isServerInfoShow = page === PageEnum.PageSettingsServerInfo || PageEnum.PageSettingsSplitTunneling || tabBar.isServerInfoShow
} }
function onGoToStartPage() { function onGoToStartPage() {
@@ -70,6 +65,21 @@ PageType {
tabBarStackView.pop() tabBarStackView.pop()
} }
} }
function onEscapePressed() {
if (!tabBar.enabled || busyIndicator.visible) {
return
}
var pageName = tabBarStackView.currentItem.objectName
if ((pageName === PageController.getPagePath(PageEnum.PageShare)) ||
(pageName === PageController.getPagePath(PageEnum.PageSettings))) {
PageController.goToPageHome()
tabBar.previousIndex = 0
} else {
PageController.closePage()
}
}
} }
Connections { Connections {
@@ -107,7 +117,7 @@ PageType {
} }
function onNoInstalledContainers() { function onNoInstalledContainers() {
PageController.setTriggeredBtConnectButton(true) PageController.setTriggeredByConnectButton(true)
ServersModel.processedIndex = ServersModel.getDefaultServerIndex() ServersModel.processedIndex = ServersModel.getDefaultServerIndex()
InstallController.setShouldCreateServer(false) InstallController.setShouldCreateServer(false)
@@ -132,7 +142,6 @@ PageType {
var pagePath = PageController.getPagePath(page) var pagePath = PageController.getPagePath(page)
tabBarStackView.clear(StackView.Immediate) tabBarStackView.clear(StackView.Immediate)
tabBarStackView.replace(pagePath, { "objectName" : pagePath }, StackView.Immediate) tabBarStackView.replace(pagePath, { "objectName" : pagePath }, StackView.Immediate)
tabBar.isServerInfoShow = false
} }
Component.onCompleted: { Component.onCompleted: {
@@ -146,7 +155,6 @@ PageType {
id: tabBar id: tabBar
property int previousIndex: 0 property int previousIndex: 0
property bool isServerInfoShow: false
anchors.right: parent.right anchors.right: parent.right
anchors.left: parent.left anchors.left: parent.left
@@ -177,7 +185,7 @@ PageType {
} }
TabImageButtonType { TabImageButtonType {
isSelected: tabBar.isServerInfoShow ? false : tabBar.currentIndex === 0 isSelected: tabBar.currentIndex === 0
image: "qrc:/images/controls/home.svg" image: "qrc:/images/controls/home.svg"
onClicked: { onClicked: {
tabBarStackView.goToTabBarPage(PageEnum.PageHome) tabBarStackView.goToTabBarPage(PageEnum.PageHome)
@@ -211,7 +219,7 @@ PageType {
} }
TabImageButtonType { TabImageButtonType {
isSelected: tabBar.isServerInfoShow ? true : tabBar.currentIndex === 2 isSelected: tabBar.currentIndex === 2
image: "qrc:/images/controls/settings-2.svg" image: "qrc:/images/controls/settings-2.svg"
onClicked: { onClicked: {
tabBarStackView.goToTabBarPage(PageEnum.PageSettings) tabBarStackView.goToTabBarPage(PageEnum.PageSettings)
+10 -4
View File
@@ -270,6 +270,7 @@ QJsonObject VpnConnection::createVpnConfiguration(int serverIndex, const ServerC
ErrorCode *errorCode) ErrorCode *errorCode)
{ {
QJsonObject vpnConfiguration; QJsonObject vpnConfiguration;
vpnConfiguration[config_key::serverIndex] = serverIndex;
for (ProtocolEnumNS::Proto proto : ContainerProps::protocolsForContainer(container)) { for (ProtocolEnumNS::Proto proto : ContainerProps::protocolsForContainer(container)) {
auto s = m_settings->server(serverIndex); auto s = m_settings->server(serverIndex);
@@ -471,10 +472,15 @@ void VpnConnection::disconnectFromVpn()
#ifdef Q_OS_ANDROID #ifdef Q_OS_ANDROID
if (m_vpnProtocol && m_vpnProtocol.data()) { if (m_vpnProtocol && m_vpnProtocol.data()) {
connect(AndroidController::instance(), &AndroidController::vpnDisconnected, this, auto *const connection = new QMetaObject::Connection;
[this]() { *connection = connect(AndroidController::instance(), &AndroidController::vpnStateChanged, this,
onConnectionStateChanged(Vpn::ConnectionState::Disconnected); [this, connection](AndroidController::ConnectionState state) {
}, Qt::SingleShotConnection); if (state == AndroidController::ConnectionState::DISCONNECTED) {
onConnectionStateChanged(Vpn::ConnectionState::Disconnected);
disconnect(*connection);
delete connection;
}
});
m_vpnProtocol.data()->stop(); m_vpnProtocol.data()->stop();
} }
#endif #endif
+1 -1
View File
@@ -20,7 +20,7 @@ PLIST_NAME=$APP_NAME.plist
# Search Qt # Search Qt
if [ -z "${QT_VERSION+x}" ]; then if [ -z "${QT_VERSION+x}" ]; then
QT_VERSION=6.5.2; QT_VERSION=6.6.2;
QT_BIN_DIR=$HOME/Qt/$QT_VERSION/ios/bin QT_BIN_DIR=$HOME/Qt/$QT_VERSION/ios/bin
fi fi
+1 -1
View File
@@ -36,7 +36,7 @@ QMAKE_STASH_FILE=$PROJECT_DIR/.qmake_stash
# Search Qt # Search Qt
if [ -z "${QT_VERSION+x}" ]; then if [ -z "${QT_VERSION+x}" ]; then
QT_VERSION=5.15.2 QT_VERSION=6.6.2
if [ -f /opt/Qt/$QT_VERSION/gcc_64/bin/qmake ]; then if [ -f /opt/Qt/$QT_VERSION/gcc_64/bin/qmake ]; then
QT_BIN_DIR=/opt/Qt/$QT_VERSION/gcc_64/bin QT_BIN_DIR=/opt/Qt/$QT_VERSION/gcc_64/bin
elif [ -f $HOME/Qt/$QT_VERSION/gcc_64/bin/qmake ]; then elif [ -f $HOME/Qt/$QT_VERSION/gcc_64/bin/qmake ]; then
+1 -1
View File
@@ -37,7 +37,7 @@ DMG_FILENAME=$PROJECT_DIR/${APP_NAME}.dmg
# Search Qt # Search Qt
if [ -z "${QT_VERSION+x}" ]; then if [ -z "${QT_VERSION+x}" ]; then
QT_VERSION=6.5.1; QT_VERSION=6.4.3;
QIF_VERSION=4.6 QIF_VERSION=4.6
QT_BIN_DIR=$HOME/Qt/$QT_VERSION/macos/bin QT_BIN_DIR=$HOME/Qt/$QT_VERSION/macos/bin
QIF_BIN_DIR=$QT_BIN_DIR/../../../Tools/QtInstallerFramework/$QIF_VERSION/bin QIF_BIN_DIR=$QT_BIN_DIR/../../../Tools/QtInstallerFramework/$QIF_VERSION/bin