Compare commits

...

134 Commits

Author SHA1 Message Date
vladimir.kuznetsov
073491ccb4 fixed anchors warning on PageHome 2024-03-25 21:52:38 +05:00
pokamest
561b62cd40 Merge pull request #705 from amnezia-vpn/bugfix/import-error-handling
fixed error handling for config import
2024-03-23 13:23:31 +00:00
pokamest
1284ed4d84 Merge pull request #706 from amnezia-vpn/translations/connection-label-fix
Fix connection button labels
2024-03-23 00:30:10 +00:00
Andrey Zaharow
6f34443191 Fix connection button labels 2024-03-21 21:34:51 +01:00
vladimir.kuznetsov
02f186c54e fixed error handling for config import 2024-03-21 23:32:11 +05:00
alexeyq2
784c6cf585 Fix AWG/WG on Linux - IPv6 gateway address is ULA now (#701) 2024-03-21 15:03:00 +00:00
pokamest
14f132e127 Merge pull request #703 from amnezia-vpn/feature/linux-ipc-fix
Increase timeout for IPC command
2024-03-21 13:29:14 +00:00
Mykola Baibuz
9cb624e681 Increase timeout for IPC command 2024-03-20 23:10:29 +02:00
isamnezia
516e3da7e2 Fix open log crash and side log improvements (#694)
Fix open log crash
2024-03-20 15:35:36 +00:00
Andrey Zaharow
0e83586cae Fix UI for Burmese language (#682)
* Fix UI for Burmese language
2024-03-20 15:20:09 +00:00
Nethius
95bdae68f4 Auto disable logs after 14 days (#610)
Auto disable logs after 14 days
2024-03-20 14:22:29 +00:00
pokamest
294778884b Merge pull request #691 from amnezia-vpn/bugfix/credentials-space-check
fixed checking credentials for spaces
2024-03-18 14:37:35 +00:00
albexk
10caecbffd Fix wg reconnection problem after awg connection (#696)
* Update Android AWG to 0.2.5
2024-03-18 11:20:01 +00:00
pokamest
553a6a73dd Merge pull request #697 from amnezia-vpn/bugfix/Service-crash-after-disconnecting
ISSUE: Service is crashed after disconnecting
2024-03-18 10:52:25 +00:00
Mykola Baibuz
e646b85e56 Setup MTU for WG/AWG protocol (#576)
Setup MTU for AWG/WG protocol
2024-03-18 10:41:53 +00:00
Dan Nguyen
b7c513c05f ISSUE: Service is crashed after disconnecting
ROOT CAUSE: When disconnecting service, m_logworker is deleted in thread which does not have affinity with m_logworker.
			The time m_logworker is deleted, it may be used by m_logthread and make the service crashed

ACTION: Connect signal finished() of m_logthread to deleteLater() slot of m_logworker to safety delete it.
2024-03-17 07:09:57 +07:00
pokamest
9f82b4c21f Merge pull request #689 from amnezia-vpn/translations/burmese-fix
Shortening of translated text in Burmese
2024-03-16 20:32:37 +00:00
pokamest
02b2da38cf Merge pull request #690 from amnezia-vpn/bugfix/native-config-import-error-handling
added error handling for importing a native config
2024-03-14 17:03:06 +00:00
vladimir.kuznetsov
f51077b2be fixed checking credentials for spaces 2024-03-14 15:59:16 +05:00
vladimir.kuznetsov
33f49bfddb added error handling for importing a native config 2024-03-14 12:55:33 +05:00
Andrey Zaharow
9a81f13f81 Short translated text 2024-03-13 22:44:09 +01:00
albexk
915fb6759a Add Android openssl3 libs, fix https connection error (#685)
Add Android openssl3 libs, fix https connection error
2024-03-13 21:22:56 +00:00
Nethius
c5a5bfde69 extended the validation of the contents of the imported file (#670)
Extended the validation of the contents of the imported file
2024-03-13 21:22:10 +00:00
Andrey Zaharow
0a90fd110d Add RU translation for Error 1101 text (#683)
* Add RU translation for Error 1101 text
2024-03-12 23:17:18 +00:00
pokamest
541d6eb0b8 Merge pull request #686 from amnezia-vpn/fix/allowips-config-change
Add AllowedIPs config change
2024-03-12 18:49:09 +00:00
pokamest
d443a0063d Merge pull request #681 from amnezia-vpn/bugfix/mobile-auto-focus-disable
First element auto-focus disabled for the mobile platforms
2024-03-12 18:48:33 +00:00
pokamest
f0c6edb670 Merge pull request #688 from amnezia-vpn/bugfix/sftp-hostname
bugfix/sftp-hostname
2024-03-12 18:47:42 +00:00
vladimir.kuznetsov
9189b53a0d fixed display of hostName on the sftp settings page 2024-03-12 23:43:24 +05:00
Igor Sorokin
fceccaefcc Add AllowedIPs config change 2024-03-12 19:57:45 +03:00
pokamest
fbeabf43ca Merge pull request #684 from amnezia-vpn/fix/android-remove-ss 2024-03-12 15:17:55 +00:00
albexk
78c7893f90 Remove shadowsocks libs from Android build 2024-03-12 17:17:38 +03:00
Garegin866
cb9a25006c - Removed additional focus frames for buttons inside text fields.
- For mobile platforms, disabled auto-focus on the first element when navigating on the page.
2024-03-12 00:02:47 +04:00
pokamest
0e87550d85 Merge pull request #672 from amnezia-vpn/translations/fix-translations
Fix translations
2024-03-10 16:18:27 -07:00
pokamest
dceb0ab832 Merge pull request #674 from amnezia-vpn/version-bump
Bump Android version code to 47
2024-03-10 04:43:44 -07:00
pokamest
a33590476a Merge pull request #677 from artromone/fix/logger
added commit hash in logger
2024-03-08 14:25:19 -08:00
Artem Romanovich
deaf618520 added commit hash in logger 2024-03-09 00:21:57 +03:00
pokamest
3d8a56d922 Merge pull request #673 from amnezia-vpn/feature/api-request-debug-output
extended debug output for api request
2024-03-07 04:43:07 -08:00
albexk
36af7cf471 Bump Android version code to 47 2024-03-07 14:27:21 +03:00
vladimir.kuznetsov
ebd3449b4a extended debug output for api request 2024-03-07 09:18:25 +03:00
Andrey Zaharow
99182f4a67 Fix translations 2024-03-06 23:04:53 +01:00
pokamest
da84ba1a4d Text fixes and some ts updates 2024-03-06 18:34:07 +00:00
pokamest
bca68fc185 iOS crash fix 2024-03-06 10:07:49 -08:00
pokamest
59a7265bac Merge pull request #671 from amnezia-vpn/bugfix/fade-on-page-start-repalce
fixed screen fade when switching from PageSetupWizardStart to PageStart
2024-03-06 06:36:07 -08:00
vladimir.kuznetsov
9201ca1e03 fixed screen fade when switching from PageSetupWizardStart to PageStart 2024-03-06 14:22:44 +05:00
dimov96
6b6a76d2cc Replace sftp with scp (#602)
Replace sftp with scp
2024-03-06 01:24:28 +00:00
isamnezia
840c388ab9 Add in-app screenshot preventing (#606)
In-app screenshot preventing fixes
2024-03-06 01:18:19 +00:00
Shehab Ahmed
5b4ec608c8 pushing the Burmese translation file (#669)
Burmese translation
2024-03-05 20:49:30 +00:00
pokamest
79ff1b81e0 Merge pull request #666 from amnezia-vpn/bugfix/Revert_PR_596
ISSUE: In start page, icon is highlighted not correctly when press ESC key
2024-03-05 05:07:05 -08:00
pokamest
ea67c01da8 Merge pull request #667 from amnezia-vpn/bugfix/http-replacement
removed the replacement of https by http in apiController
2024-03-04 13:07:06 -08:00
vladimir.kuznetsov
1137e169ea removed the replacement of https by http in apiController 2024-03-04 21:45:04 +03:00
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
pokamest
13aadbda64 Merge pull request #637 from amnezia-vpn/bugfix/connection-drawer-close-button
fixed connection drawer close button
2024-02-23 09:56:52 -08:00
agalehaga
c7c7c8eb01 added export awg native format (#635)
add export awg native format
2024-02-23 17:55:59 +00:00
vladimir.kuznetsov
b1e5bba33f fixed connection drawer close button 2024-02-23 22:51:46 +05:00
pokamest
474e7c6d62 Merge pull request #634 from amnezia-vpn/update/gh_actions_qt_update
Update Qt in deploy.yml
2024-02-22 06:02:04 -08:00
pokamest
794ec921b8 Update Qt in deploy.yml 2024-02-22 13:28:37 +00:00
pokamest
b674240362 Merge pull request #632 from amnezia-vpn/refactoring/changing-settings-item-location
moving settings item to other settings page
2024-02-22 05:02:13 -08:00
pokamest
a768c7c451 Merge pull request #633 from rodionos/patch-1
MacOS build: increase image size to 256Mb
2024-02-22 04:58:21 -08:00
Sergei Rodionov
28d2a4ec2c MacOS build: increase image size to 256Mb
In my case, using Qt 6.6.2, the size of the AmneziaVPN.dmg file is 226Mb so a higher image size is needed for the hdiutil command.
2024-02-22 13:57:58 +03:00
Shehab Ahmed
9f1210d18f changed the location of Auto connect item from settings connections page to settings application page 2024-02-22 02:31:51 +02:00
pokamest
3012559627 Merge pull request #630 from amnezia-vpn/feature/api-containers-listview
for api servers, removed the ability to select a container
2024-02-21 11:03:18 -08:00
vladimir.kuznetsov
b3ed57aee7 for api servers, removed the ability to select a container 2024-02-21 23:41:47 +05:00
pokamest
89d0a8107d Merge pull request #620 from amnezia-vpn/translations/fix-for-pr618
Fix translation for #618
2024-02-21 06:03:12 -08:00
Andrey Zaharow
6c0b71bd1b Fix translation on About Page (#618)
Fix About Page
2024-02-21 14:01:53 +00:00
Nethius
61abf74b2d feature/page-home-split-tunneling (#540)
Added split tunneling button on home page
2024-02-21 11:27:27 +00:00
pokamest
21fdf02921 Merge pull request #625 from amnezia-vpn/bugfix/default-server-default-container-update
fixed the use of defaultServerDefaultContainerChanged
2024-02-21 03:22:09 -08:00
vladimir.kuznetsov
7a245d80ee fixed the use of defaultServerDefaultContainerChanged 2024-02-21 13:06:39 +05:00
KsZnak
da85922f23 Update amneziavpn_zh_CN.ts (#617)
Update amneziavpn_zh_CN.ts
2024-02-20 20:49:26 +00:00
pokamest
a5356b6319 Merge pull request #623 from amnezia-vpn/update/Arabic-translation
updated the Arabic translation for fixing some sentences
2024-02-20 12:47:41 -08:00
KsZnak
3828891b9b Update amneziavpn_fa_IR.ts (#622)
Update amneziavpn_fa_IR.ts
2024-02-20 20:46:23 +00:00
pokamest
15d866ce04 WG/AWG ipv6 fix (#621)
WG/AWG ipv6 fix
2024-02-20 19:05:36 +00:00
Shehab Ahmed
560eb3d620 updated the Arabic translation for fixing some sentences 2024-02-20 20:37:19 +02:00
Andrey Zaharow
ac894254cc Fix translation for #618 2024-02-20 00:23:20 +01:00
pokamest
17e3fbde25 Merge pull request #616 from amnezia-vpn/bugfix/cursor-changing-fix
Fix cursor change when hover over elements
2024-02-19 12:20:46 -08:00
Andrey Zaharow
ee11a8410c Fix cursor change when hover over elements 2024-02-19 18:28:29 +01:00
pokamest
ff5c51cfd9 Merge pull request #615 from amnezia-vpn/KsZnak-patch-1
Update amneziavpn_ru.ts
2024-02-19 07:10:49 -08:00
Nethius
b3943ae5e3 serversModel cleanup (#599) 2024-02-19 14:54:15 +00:00
pokamest
a32952fde6 Qt.ImhNoAutoUppercase | Qt.ImhSensitiveData | Qt.ImhNoPredictiveText for
all TextFields
2024-02-19 14:06:18 +00:00
isamnezia
9c4ee4014d Fix for Codacy: variable name should be between 3 and 40 characters long (#608)
Tiny fixes for iOS
2024-02-19 13:13:10 +00:00
KsZnak
dc9069f1f4 Update_2_amneziavpn_ru.ts
Add new change
2024-02-19 13:37:40 +02:00
pokamest
e402cacc05 Merge pull request #614 from amnezia-vpn/bugfix/translations
returned translation files to commit fab167bb34a9f7199359e3d8589a1cd1…
2024-02-19 02:23:39 -08:00
vladimir.kuznetsov
a98cd248d6 returned translation files to commit fab167bb34 2024-02-19 09:31:31 +05:00
pokamest
00fbfb6a01 Merge pull request #611 from amnezia-vpn/refactoring/show-installed-containers-first
show installed protocols first
2024-02-18 11:10:37 -08:00
vladimir.kuznetsov
86c31c3766 show installed protocols first in services tab and page home containers listview 2024-02-18 13:24:21 +05:00
agalehaga
698cfe910c add navigation using enter + buttons will be clicked if enter (if but… (#556)
Enter navigation
2024-02-17 21:09:05 +00:00
pokamest
16db23c159 Rewrite sftp file copy to Qt way (#562)
Rewrite sftp file copy to Qt way
2024-02-17 21:07:17 +00:00
Andrey Zaharow
b05a5ee1c6 fix connection button behavior (#595)
Fix connection button behavior
2024-02-17 19:57:31 +00:00
pokamest
8cb298937f Merge pull request #604 from amnezia-vpn/KsZnak-ru_translate
Update amneziavpn_ru.ts
2024-02-17 11:52:18 -08:00
Andrey Zaharow
68fe20ddf6 UI fixes (#596)
UI fixes
2024-02-17 19:48:41 +00:00
KsZnak
fab167bb34 Update amneziavpn_ru.ts 2024-02-17 20:29:25 +02:00
isamnezia
f640d4b5f5 Remove config string dependency (#577)
Remove WG/AWG config string dependency
2024-02-16 10:30:00 +00:00
Nethius
074562b141 feature/custom-drawer (#563)
Replaced all the DrawerType with DrawerType2
2024-02-16 10:24:06 +00:00
Shehab Ahmed
fd030a5fd4 Arabic translation (#594)
added Arabic translation
2024-02-16 10:19:47 +00:00
albexk
82fa6b13c6 Fix foreground service type (#592)
Fix foreground service type
2024-02-14 16:35:40 +00:00
pokamest
bf16298c40 Version bump - 4.4.0.0 2024-02-13 21:10:47 +00:00
pokamest
bcebb0a2b5 Merge pull request #580 from amnezia-vpn/feature/update-cloak-binary
Update AWG and Cloak libraries
2024-02-13 12:03:02 -08:00
pokamest
b27442cf74 Merge pull request #583 from amnezia-vpn/bugfix/double_clear_server_from_amnezia
fixed bug with double button clear server from amnezia software
2024-02-13 07:50:35 -08:00
Nethius
92fbbd4812 bugfix/default-container-index (#578)
fixed get/set DefaultContainer
2024-02-13 15:20:13 +00:00
agalehaga
321ed810e3 fixed bug with double button clear server from amnezia software 2024-02-13 15:16:04 +02:00
albexk
17ff530683 Merge branch 'fix/android' into feature/update-cloak-binary 2024-02-13 12:32:36 +03:00
pokamest
a416d03614 Merge pull request #581 from amnezia-vpn/fix/amn-go-version
Update amneziawg-apple to amneziawg-go v0.2.1
2024-02-12 13:08:19 -08:00
Igor Sorokin
4de9a274dd Update amneziawg-apple to amneziawg-go v0.2.1 2024-02-12 23:25:11 +03:00
Mykola Baibuz
0b8f3c9d9d Update Cloak binary to v2.8.0 2024-02-12 21:01:44 +02:00
171 changed files with 12871 additions and 4279 deletions

View File

@@ -14,8 +14,8 @@ jobs:
runs-on: ubuntu-20.04
env:
QT_VERSION: 6.5.1
QIF_VERSION: 4.6
QT_VERSION: 6.6.2
QIF_VERSION: 4.7
steps:
- name: 'Install Qt'
@@ -72,8 +72,8 @@ jobs:
runs-on: windows-latest
env:
QT_VERSION: 6.5.1
QIF_VERSION: 4.6
QT_VERSION: 6.6.2
QIF_VERSION: 4.7
BUILD_ARCH: 64
steps:
@@ -134,7 +134,7 @@ jobs:
runs-on: macos-13
env:
QT_VERSION: 6.5.2
QT_VERSION: 6.6.2
CC: cc
CXX: c++
@@ -245,10 +245,15 @@ jobs:
modules: 'qtremoteobjects qt5compat qtshadertools'
dir: ${{ runner.temp }}
setup-python: 'true'
tools: 'tools_ifw'
set-env: 'true'
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'
uses: actions/checkout@v4
with:
@@ -286,7 +291,7 @@ jobs:
env:
ANDROID_BUILD_PLATFORM: android-34
QT_VERSION: 6.6.1
QT_VERSION: 6.6.2
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
steps:

3
.gitignore vendored
View File

@@ -131,3 +131,6 @@ client/3rd/ShadowSocks/ss_ios.xcconfig
# UML generated pics
out/
# CMake files
CMakeFiles/

View File

@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
set(PROJECT AmneziaVPN)
project(${PROJECT} VERSION 4.3.0.0
project(${PROJECT} VERSION 4.4.2.1
DESCRIPTION "AmneziaVPN"
HOMEPAGE_URL "https://amnezia.org/"
)
@@ -11,7 +11,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
set(RELEASE_DATE "${CURRENT_DATE}")
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
set(APP_ANDROID_VERSION_CODE 44)
set(APP_ANDROID_VERSION_CODE 49)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux")

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.
## 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
- 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.
## Links
[https://amnezia.org](https://amnezia.org) - project website
[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)
@@ -21,13 +23,13 @@ Amnezia is an open-source VPN client, with a key feature that enables you to dep
## Tech
AmneziaVPN uses a number of open source projects to work:
AmneziaVPN uses several open-source projects to work:
- [OpenSSL](https://www.openssl.org/)
- [OpenVPN](https://openvpn.net/)
- [ShadowSocks](https://shadowsocks.org/)
- [Qt](https://www.qt.io/)
- [LibSsh](https://libssh.org) - forked form Qt Creator
- [LibSsh](https://libssh.org) - forked from Qt Creator
- and more...
## Checking out the source code
@@ -43,14 +45,15 @@ git submodule update --init --recursive
Want to contribute? Welcome!
### 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.
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:
- macOS
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
- iOS
- Qt 5 Compatibility Module
- Qt Shader Tools
@@ -59,18 +62,18 @@ Look deploy folder for build scripts.
- Qt Multimedia
- 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.
Latest version is recommended. Install gomobile
The latest version is recommended. Install gomobile
```bash
export PATH=$PATH:~/go/bin
go install golang.org/x/mobile/cmd/gomobile@latest
gomobile init
```
5. Build project
5. Build the project
```bash
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"
@@ -88,62 +91,63 @@ of the bin folder where gomobile was installed. Usually, it's in `GOPATH`.
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: ***
[$(PROJECTDIR)/client/build/AmneziaVPN.build/Debug-iphoneos/wireguard-go-bridge/goroot/.prepared]
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`.
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
```
Build might fail with "source files not found" error the first time you try it, because modern XCode build system compiles
dependencies in parallel, and some dependencies end up being built after the ones that
require them. In this case simply restart the build.
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
require them. In this case, simply restart the build.
## How to build the Android app
_tested on Mac OS_
_Tested on Mac OS_
The Android app has the following requirements:
* JDK 11
* 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`.
* set path to jdk 11
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 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.
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!
That's it! You should be ready to compile the project from QT Creator!
### 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
GPL v.3
GPL v3.0
## Donate
Bitcoin: bc1qn9rhsffuxwnhcuuu4qzrwp4upkrq94xnh8r26u
XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3
payeer.com: P2561305
ko-fi.com: [https://ko-fi.com/amnezia_vpn](https://ko-fi.com/amnezia_vpn)
## Acknowledgments
## etc
This project is tested with BrowserStack.
We express our gratitude to [BrowserStack](https://www.browserstack.com) for supporting our project.

View File

@@ -15,6 +15,15 @@ set(PACKAGES
Core5Compat Concurrent LinguistTools
)
execute_process(
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
COMMAND git rev-parse --short HEAD
OUTPUT_VARIABLE GIT_COMMIT_HASH
OUTPUT_STRIP_TRAILING_WHITESPACE
)
add_definitions(-DGIT_COMMIT_HASH="${GIT_COMMIT_HASH}")
if(IOS)
set(PACKAGES ${PACKAGES} Multimedia)
endif()
@@ -57,6 +66,8 @@ set(AMNEZIAVPN_TS_FILES
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_ru.ts
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_zh_CN.ts
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_fa_IR.ts
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_ar.ts
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_my_MM.ts
)
file(GLOB_RECURSE AMNEZIAVPN_TS_SOURCES *.qrc *.cpp *.h *.ui)

View File

@@ -24,6 +24,7 @@
#if defined(Q_OS_IOS)
#include "platforms/ios/ios_controller.h"
#include <AmneziaVPN-Swift.h>
#endif
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
@@ -95,7 +96,18 @@ void AmneziaApplication::init()
qFatal("Android logging initialization failed");
}
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);
AndroidController::instance()->setScreenshotsEnabled(m_settings->isScreenshotsEnabled());
connect(m_settings.get(), &Settings::screenshotsEnabledChanged,
AndroidController::instance(), &AndroidController::setScreenshotsEnabled);
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,
[this](Vpn::ConnectionState state) {
@@ -127,6 +139,14 @@ void AmneziaApplication::init()
m_pageController->goToPageSettingsBackup();
m_settingsController->importBackupFromOutside(filePath);
});
QTimer::singleShot(0, this, [this](){
AmneziaVPN::toggleScreenshots(m_settings->isScreenshotsEnabled());
});
connect(m_settings.get(), &Settings::screenshotsEnabledChanged, [](bool enabled) {
AmneziaVPN::toggleScreenshots(enabled);
});
#endif
m_notificationHandler.reset(NotificationHandler::create(nullptr));
@@ -286,13 +306,16 @@ void AmneziaApplication::initModels()
m_containersModel.reset(new ContainersModel(this));
m_engine->rootContext()->setContextProperty("ContainersModel", m_containersModel.get());
m_defaultServerContainersModel.reset(new ContainersModel(this));
m_engine->rootContext()->setContextProperty("DefaultServerContainersModel", m_defaultServerContainersModel.get());
m_serversModel.reset(new ServersModel(m_settings, this));
m_engine->rootContext()->setContextProperty("ServersModel", m_serversModel.get());
connect(m_serversModel.get(), &ServersModel::containersUpdated, m_containersModel.get(),
&ContainersModel::updateModel);
connect(m_serversModel.get(), &ServersModel::defaultContainerChanged, m_containersModel.get(),
&ContainersModel::setDefaultContainer);
m_containersModel->setDefaultContainer(m_serversModel->getDefaultContainer()); // make better?
connect(m_serversModel.get(), &ServersModel::defaultServerContainersUpdated, m_defaultServerContainersModel.get(),
&ContainersModel::updateModel);
m_serversModel->resetModel();
m_languageModel.reset(new LanguageModel(m_settings, this));
m_engine->rootContext()->setContextProperty("LanguageModel", m_languageModel.get());
@@ -336,7 +359,7 @@ void AmneziaApplication::initModels()
connect(m_configurator.get(), &VpnConfigurator::newVpnConfigCreated, this,
[this](const QString &clientId, const QString &clientName, const DockerContainer container,
ServerCredentials credentials) {
m_serversModel->reloadContainerConfig();
m_serversModel->reloadDefaultServerContainerConfig();
m_clientManagementModel->appendClient(clientId, clientName, container, credentials);
emit m_configurator->clientModelUpdated();
});
@@ -388,7 +411,13 @@ void AmneziaApplication::initControllers()
m_engine->rootContext()->setContextProperty("ApiController", m_apiController.get());
connect(m_apiController.get(), &ApiController::updateStarted, this,
[this]() { emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Connecting); });
connect(m_apiController.get(), &ApiController::errorOccurred, this,
[this]() { emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected); });
connect(m_apiController.get(), &ApiController::updateFinished, m_connectionController.get(), &ConnectionController::toggleConnection);
connect(m_apiController.get(), &ApiController::errorOccurred, this, [this](const QString &errorMessage) {
if (m_connectionController->isConnectionInProgress()) {
emit m_pageController->showErrorMessage(errorMessage);
}
emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected);
});
connect(m_apiController.get(), &ApiController::updateFinished, m_connectionController.get(),
&ConnectionController::toggleConnection);
}

View File

@@ -92,6 +92,7 @@ private:
QCommandLineParser m_parser;
QSharedPointer<ContainersModel> m_containersModel;
QSharedPointer<ContainersModel> m_defaultServerContainersModel;
QSharedPointer<ServersModel> m_serversModel;
QSharedPointer<LanguageModel> m_languageModel;
QSharedPointer<ProtocolsModel> m_protocolsModel;

View File

@@ -22,7 +22,7 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- Enable when VPN-per-app mode will be implemented -->
@@ -56,6 +56,10 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</intent-filter>
<meta-data
android:name="android.app.lib_name"
android:value="-- %%INSERT_APP_LIB_NAME%% --" />
@@ -137,14 +141,29 @@
android:name=".AmneziaVpnService"
android:process=":amneziaVpnService"
android:permission="android.permission.BIND_VPN_SERVICE"
android:foregroundServiceType="specialUse"
android:exported="false">
android:foregroundServiceType="systemExempted"
android:exported="false"
tools:ignore="ForegroundServicePermission">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
</service>
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" android:value="vpn" />
<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

View File

@@ -64,7 +64,7 @@ class Awg : Wireguard() {
val configDataJson = config.getJSONObject("awg_config_data")
val configData = parseConfigData(configDataJson.getString("config"))
return AwgConfig.build {
configWireguard(configData)
configWireguard(configData, configDataJson)
configSplitTunneling(config)
configData["Jc"]?.let { setJc(it.toInt()) }
configData["Jmin"]?.let { setJmin(it.toInt()) }

View File

@@ -111,4 +111,5 @@ dependencies {
implementation(libs.kotlinx.coroutines)
implementation(libs.bundles.androidx.camera)
implementation(libs.google.mlkit)
implementation(libs.androidx.datastore)
}

View File

@@ -6,6 +6,7 @@ androidx-activity = "1.8.1"
androidx-annotation = "1.7.0"
androidx-camera = "1.3.0"
androidx-security-crypto = "1.1.0-alpha06"
androidx-datastore = "1.1.0-beta01"
kotlinx-coroutines = "1.7.3"
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-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-datastore = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-datastore" }
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" }

View File

@@ -2,9 +2,9 @@ package org.amnezia.vpn.protocol
// keep synchronized with client/platforms/android/android_controller.h ConnectionState
enum class ProtocolState {
DISCONNECTED,
CONNECTED,
CONNECTING,
DISCONNECTED,
DISCONNECTING,
RECONNECTING,
UNKNOWN

View File

@@ -28,6 +28,10 @@ fun Bundle.putStatus(status: Status) {
putInt(STATE_KEY, status.state.ordinal)
}
fun Bundle.putStatus(state: ProtocolState) {
putInt(STATE_KEY, state.ordinal)
}
fun Bundle.getStatus(): Status =
Status.build {
setState(ProtocolState.entries[getInt(STATE_KEY)])

View File

@@ -0,0 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="connecting">Подключение</string>
<string name="disconnecting">Отключение</string>
</resources>

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>

View File

@@ -14,6 +14,7 @@ import android.os.IBinder
import android.os.Looper
import android.os.Message
import android.os.Messenger
import android.view.WindowManager.LayoutParams
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.annotation.MainThread
@@ -26,9 +27,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.amnezia.vpn.protocol.ProtocolState
import org.amnezia.vpn.protocol.getStatistics
import org.amnezia.vpn.protocol.getStatus
import org.amnezia.vpn.qt.QtAndroidController
@@ -36,11 +35,11 @@ import org.amnezia.vpn.util.Log
import org.qtproject.qt.android.bindings.QtActivity
private const val TAG = "AmneziaActivity"
const val ACTIVITY_MESSENGER_NAME = "Activity"
private const val CHECK_VPN_PERMISSION_ACTION_CODE = 1
private const val CREATE_FILE_ACTION_CODE = 2
private const val OPEN_FILE_ACTION_CODE = 3
private const val BIND_SERVICE_TIMEOUT = 1000L
class AmneziaActivity : QtActivity() {
@@ -58,25 +57,17 @@ class AmneziaActivity : QtActivity() {
val event = msg.extractIpcMessage<ServiceEvent>()
Log.d(TAG, "Handle event: $event")
when (event) {
ServiceEvent.CONNECTED -> {
QtAndroidController.onVpnConnected()
}
ServiceEvent.DISCONNECTED -> {
QtAndroidController.onVpnDisconnected()
doUnbindService()
}
ServiceEvent.RECONNECTING -> {
QtAndroidController.onVpnReconnecting()
ServiceEvent.STATUS_CHANGED -> {
msg.data?.getStatus()?.let { (state) ->
Log.d(TAG, "Handle protocol state: $state")
QtAndroidController.onVpnStateChanged(state.ordinal)
}
}
ServiceEvent.STATUS -> {
if (isWaitingStatus) {
isWaitingStatus = false
msg.data?.getStatus()?.let { (state) ->
QtAndroidController.onStatus(state.ordinal)
}
msg.data?.getStatus()?.let { QtAndroidController.onStatus(it) }
}
}
@@ -87,7 +78,7 @@ class AmneziaActivity : QtActivity() {
}
ServiceEvent.ERROR -> {
msg.data?.getString(ERROR_MSG)?.let { error ->
msg.data?.getString(MSG_ERROR)?.let { error ->
Log.e(TAG, "From VpnService: $error")
}
// todo: add error reporting to Qt
@@ -109,14 +100,15 @@ class AmneziaActivity : QtActivity() {
// get a messenger from the service to send actions to the service
vpnServiceMessenger.set(Messenger(service))
// send a messenger to the service to process service events
vpnServiceMessenger.send {
Action.REGISTER_CLIENT.packToMessage().apply {
replyTo = activityMessenger
}
}
vpnServiceMessenger.send(
Action.REGISTER_CLIENT.packToMessage {
putString(MSG_CLIENT_NAME, ACTIVITY_MESSENGER_NAME)
},
replyTo = activityMessenger
)
isServiceConnected = true
if (isWaitingStatus) {
vpnServiceMessenger.send(Action.REQUEST_STATUS)
vpnServiceMessenger.send(Action.REQUEST_STATUS, replyTo = activityMessenger)
}
}
@@ -126,6 +118,7 @@ class AmneziaActivity : QtActivity() {
vpnServiceMessenger.reset()
isWaitingStatus = true
QtAndroidController.onServiceDisconnected()
doBindService()
}
override fun onBindingDied(name: ComponentName?) {
@@ -148,8 +141,11 @@ class AmneziaActivity : QtActivity() {
Log.d(TAG, "Create Amnezia activity: $intent")
mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
vpnServiceMessenger = IpcMessenger(
onDeadObjectException = ::doUnbindService,
messengerName = "VpnService"
"VpnService",
onDeadObjectException = {
doUnbindService()
doBindService()
}
)
intent?.let(::processIntent)
}
@@ -244,10 +240,9 @@ class AmneziaActivity : QtActivity() {
private fun doBindService() {
Log.d(TAG, "Bind service")
Intent(this, AmneziaVpnService::class.java).also {
bindService(it, serviceConnection, BIND_ABOVE_CLIENT)
bindService(it, serviceConnection, BIND_ABOVE_CLIENT and BIND_AUTO_CREATE)
}
isInBoundState = true
handleBindTimeout()
}
@MainThread
@@ -256,26 +251,14 @@ class AmneziaActivity : QtActivity() {
Log.d(TAG, "Unbind service")
isWaitingStatus = true
QtAndroidController.onServiceDisconnected()
vpnServiceMessenger.reset()
isServiceConnected = false
vpnServiceMessenger.send(Action.UNREGISTER_CLIENT, activityMessenger)
vpnServiceMessenger.reset()
isInBoundState = false
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
*/
@@ -312,7 +295,7 @@ class AmneziaActivity : QtActivity() {
Log.d(TAG, "Connect to VPN")
vpnServiceMessenger.send {
Action.CONNECT.packToMessage {
putString(VPN_CONFIG, vpnConfig)
putString(MSG_VPN_CONFIG, vpnConfig)
}
}
}
@@ -320,7 +303,7 @@ class AmneziaActivity : QtActivity() {
private fun startVpnService(vpnConfig: String) {
Log.d(TAG, "Start VPN service")
Intent(this, AmneziaVpnService::class.java).apply {
putExtra(VPN_CONFIG, vpnConfig)
putExtra(MSG_VPN_CONFIG, vpnConfig)
}.also {
ContextCompat.startForegroundService(this, it)
}
@@ -369,6 +352,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")
fun saveFile(fileName: String, data: String) {
Log.d(TAG, "Save file $fileName")
@@ -438,7 +437,7 @@ class AmneziaActivity : QtActivity() {
Log.saveLogs = enabled
vpnServiceMessenger.send {
Action.SET_SAVE_LOGS.packToMessage {
putBoolean(SAVE_LOGS, enabled)
putBoolean(MSG_SAVE_LOGS, enabled)
}
}
}
@@ -455,4 +454,13 @@ class AmneziaActivity : QtActivity() {
Log.v(TAG, "Clear logs")
Log.clearLogs()
}
@Suppress("unused")
fun setScreenshotsEnabled(enabled: Boolean) {
Log.v(TAG, "Set screenshots enabled: $enabled")
mainScope.launch {
val flag = if (enabled) 0 else LayoutParams.FLAG_SECURE
window.setFlags(flag, LayoutParams.FLAG_SECURE)
}
}
}

View File

@@ -18,6 +18,7 @@ class AmneziaApplication : QtApplication(), CameraXConfig.Provider {
super.onCreate()
Prefs.init(this)
Log.init(this)
VpnStateStore.init(this)
Log.d(TAG, "Create Amnezia application")
createNotificationChannel()
}

View File

@@ -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
}
}

View File

@@ -1,10 +1,13 @@
package org.amnezia.vpn
import android.app.ActivityManager
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE
import android.app.Notification
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED
import android.net.VpnService
import android.os.Build
import android.os.Handler
@@ -16,6 +19,7 @@ import android.os.Process
import androidx.annotation.MainThread
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import java.util.concurrent.ConcurrentHashMap
import kotlin.LazyThreadSafetyMode.NONE
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
@@ -26,6 +30,7 @@ import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
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.RECONNECTING
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.VpnStartException
import org.amnezia.vpn.protocol.awg.Awg
import org.amnezia.vpn.protocol.cloak.Cloak
import org.amnezia.vpn.protocol.openvpn.OpenVpn
import org.amnezia.vpn.protocol.putStatistics
import org.amnezia.vpn.protocol.putStatus
import org.amnezia.vpn.protocol.wireguard.Wireguard
import org.amnezia.vpn.util.Log
@@ -57,12 +59,16 @@ import org.json.JSONObject
private const val TAG = "AmneziaVpnService"
const val VPN_CONFIG = "VPN_CONFIG"
const val ERROR_MSG = "ERROR_MSG"
const val SAVE_LOGS = "SAVE_LOGS"
const val MSG_VPN_CONFIG = "VPN_CONFIG"
const val MSG_ERROR = "ERROR"
const val MSG_SAVE_LOGS = "SAVE_LOGS"
const val MSG_CLIENT_NAME = "CLIENT_NAME"
const val AFTER_PERMISSION_CHECK = "AFTER_PERMISSION_CHECK"
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 STATISTICS_SENDING_TIMEOUT = 1000L
private const val DISCONNECT_TIMEOUT = 5000L
@@ -76,6 +82,8 @@ class AmneziaVpnService : VpnService() {
private var protocol: Protocol? = null
private val protocolCache = mutableMapOf<String, Protocol>()
private var protocolState = MutableStateFlow(UNKNOWN)
private var serverName: String? = null
private var serverIndex: Int = -1
private val isConnected
get() = protocolState.value == CONNECTED
@@ -89,8 +97,11 @@ class AmneziaVpnService : VpnService() {
private var connectionJob: Job? = null
private var disconnectionJob: Job? = null
private var statisticsSendingJob: Job? = null
private lateinit var clientMessenger: IpcMessenger
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 ->
protocolState.value = DISCONNECTED
@@ -116,13 +127,22 @@ class AmneziaVpnService : VpnService() {
Log.d(TAG, "Handle action: $action")
when (action) {
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 -> {
val vpnConfig = msg.data.getString(VPN_CONFIG)
Prefs.save(PREFS_CONFIG_KEY, vpnConfig)
connect(vpnConfig)
connect(msg.data.getString(MSG_VPN_CONFIG))
}
Action.DISCONNECT -> {
@@ -130,17 +150,17 @@ class AmneziaVpnService : VpnService() {
}
Action.REQUEST_STATUS -> {
clientMessenger.send {
ServiceEvent.STATUS.packToMessage {
putStatus(Status.build {
setState(this@AmneziaVpnService.protocolState.value)
})
clientMessengers[msg.replyTo]?.let { clientMessenger ->
clientMessenger.send {
ServiceEvent.STATUS.packToMessage {
putStatus(this@AmneziaVpnService.protocolState.value)
}
}
}
}
Action.SET_SAVE_LOGS -> {
Log.saveLogs = msg.data.getBoolean(SAVE_LOGS)
Log.saveLogs = msg.data.getBoolean(MSG_SAVE_LOGS)
}
}
}
@@ -156,7 +176,7 @@ class AmneziaVpnService : VpnService() {
*/
private val foregroundServiceTypeCompat
get() = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> FOREGROUND_SERVICE_TYPE_SPECIAL_USE
Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> FOREGROUND_SERVICE_TYPE_MANIFEST
else -> 0
}
@@ -189,7 +209,7 @@ class AmneziaVpnService : VpnService() {
Log.d(TAG, "Create Amnezia VPN service")
mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
connectionScope = CoroutineScope(SupervisorJob() + Dispatchers.IO + connectionExceptionHandler)
clientMessenger = IpcMessenger(messengerName = "Client")
loadServerData()
launchProtocolStateHandler()
networkState = NetworkState(this, ::reconnect)
}
@@ -201,15 +221,13 @@ class AmneziaVpnService : VpnService() {
if (isAlwaysOnCompat) {
Log.d(TAG, "Start service via Always-on")
connect(Prefs.load(PREFS_CONFIG_KEY))
connect()
} else if (intent?.getBooleanExtra(AFTER_PERMISSION_CHECK, false) == true) {
Log.d(TAG, "Start service after permission check")
connect(Prefs.load(PREFS_CONFIG_KEY))
connect()
} else {
Log.d(TAG, "Start service")
val vpnConfig = intent?.getStringExtra(VPN_CONFIG)
Prefs.save(PREFS_CONFIG_KEY, vpnConfig)
connect(vpnConfig)
connect(intent?.getStringExtra(MSG_VPN_CONFIG))
}
ServiceCompat.startForeground(this, NOTIFICATION_ID, notification, foregroundServiceTypeCompat)
return START_REDELIVER_INTENT
@@ -219,17 +237,16 @@ class AmneziaVpnService : VpnService() {
Log.d(TAG, "onBind by $intent")
if (intent?.action == SERVICE_INTERFACE) return super.onBind(intent)
isServiceBound = true
if (isConnected) launchSendingStatistics()
return vpnServiceMessenger.binder
}
override fun onUnbind(intent: Intent?): Boolean {
Log.d(TAG, "onUnbind by $intent")
if (intent?.action != SERVICE_INTERFACE) {
isServiceBound = false
stopSendingStatistics()
clientMessenger.reset()
if (isUnknown || isDisconnected) stopService()
if (clientMessengers.isEmpty()) {
isServiceBound = false
if (isUnknown || isDisconnected) stopService()
}
}
return true
}
@@ -238,7 +255,6 @@ class AmneziaVpnService : VpnService() {
Log.d(TAG, "onRebind by $intent")
if (intent?.action != SERVICE_INTERFACE) {
isServiceBound = true
if (isConnected) launchSendingStatistics()
}
super.onRebind(intent)
}
@@ -278,17 +294,16 @@ class AmneziaVpnService : VpnService() {
*/
private fun launchProtocolStateHandler() {
mainScope.launch {
protocolState.collect { protocolState ->
// drop first default UNKNOWN state
protocolState.drop(1).collect { protocolState ->
Log.d(TAG, "Protocol state changed: $protocolState")
when (protocolState) {
CONNECTED -> {
clientMessenger.send(ServiceEvent.CONNECTED)
networkState.bindNetworkListener()
if (isServiceBound) launchSendingStatistics()
if (isActivityConnected) launchSendingStatistics()
}
DISCONNECTED -> {
clientMessenger.send(ServiceEvent.DISCONNECTED)
networkState.unbindNetworkListener()
stopSendingStatistics()
if (!isServiceBound) stopService()
@@ -300,12 +315,19 @@ class AmneziaVpnService : VpnService() {
}
RECONNECTING -> {
clientMessenger.send(ServiceEvent.RECONNECTING)
stopSendingStatistics()
}
CONNECTING, UNKNOWN -> {}
}
clientMessengers.send {
ServiceEvent.STATUS_CHANGED.packToMessage {
putStatus(protocolState)
}
}
VpnStateStore.store { VpnState(protocolState, serverName, serverIndex) }
}
}
}
@@ -332,7 +354,17 @@ class AmneziaVpnService : VpnService() {
}
@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
Log.d(TAG, "Start VPN connection")
@@ -340,6 +372,7 @@ class AmneziaVpnService : VpnService() {
protocolState.value = CONNECTING
val config = parseConfigToJson(vpnConfig)
saveServerData(config)
if (config == null) {
onError("Invalid VPN config")
protocolState.value = DISCONNECTED
@@ -417,24 +450,38 @@ class AmneziaVpnService : VpnService() {
private fun onError(msg: String) {
Log.e(TAG, msg)
mainScope.launch {
clientMessenger.send {
clientMessengers.send {
ServiceEvent.ERROR.packToMessage {
putString(ERROR_MSG, msg)
putString(MSG_ERROR, msg)
}
}
}
}
private fun parseConfigToJson(vpnConfig: String?): JSONObject? =
try {
vpnConfig?.let {
JSONObject(it)
}
} catch (e: JSONException) {
onError("Invalid VPN config json format: ${e.message}")
private fun parseConfigToJson(vpnConfig: String): JSONObject? =
if (vpnConfig.isBlank()) {
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 =
if (prepare(applicationContext) != null) {
Intent(this, VpnRequestActivity::class.java).apply {
@@ -446,4 +493,12 @@ class AmneziaVpnService : VpnService() {
} else {
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
}
}
}

View File

@@ -20,9 +20,7 @@ sealed interface IpcMessage {
}
enum class ServiceEvent : IpcMessage {
CONNECTED,
DISCONNECTED,
RECONNECTING,
STATUS_CHANGED,
STATUS,
STATISTICS_UPDATE,
ERROR
@@ -30,6 +28,7 @@ enum class ServiceEvent : IpcMessage {
enum class Action : IpcMessage {
REGISTER_CLIENT,
UNREGISTER_CLIENT,
CONNECT,
DISCONNECT,
REQUEST_STATUS,

View File

@@ -9,11 +9,21 @@ import org.amnezia.vpn.util.Log
private const val TAG = "IpcMessenger"
class IpcMessenger(
messengerName: String? = null,
private val onDeadObjectException: () -> Unit = {},
private val onRemoteException: () -> Unit = {},
private val messengerName: String = "Unknown"
private val onRemoteException: () -> Unit = {}
) {
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) {
this.messenger = messenger
@@ -25,19 +35,29 @@ class IpcMessenger(
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)
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) {
try {
send(msg)
} catch (e: DeadObjectException) {
Log.w(TAG, "$messengerName messenger is dead")
Log.w(TAG, "$name messenger is dead")
messenger = null
onDeadObjectException()
} 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()
}
}
}
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) }

View File

@@ -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)
}
}
}
}

View File

@@ -1,18 +1,23 @@
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,
* called by events in the Android part of the client
*/
object QtAndroidController {
fun onStatus(status: Status) = onStatus(status.state)
fun onStatus(protocolState: ProtocolState) = onStatus(protocolState.ordinal)
external fun onStatus(stateCode: Int)
external fun onServiceDisconnected()
external fun onServiceError()
external fun onVpnPermissionRejected()
external fun onVpnConnected()
external fun onVpnDisconnected()
external fun onVpnReconnecting()
external fun onVpnStateChanged(stateCode: Int)
external fun onStatisticsUpdate(rxBytes: Long, txBytes: Long)
external fun onFileOpened(uri: String)

View File

@@ -1,10 +0,0 @@
package org.amnezia.vpn.protocol.wireguard
object GoBackend {
external fun wgGetConfig(handle: Int): String?
external fun wgGetSocketV4(handle: Int): Int
external fun wgGetSocketV6(handle: Int): Int
external fun wgTurnOff(handle: Int)
external fun wgTurnOn(ifName: String, tunFd: Int, settings: String): Int
external fun wgVersion(): String
}

View File

@@ -0,0 +1,10 @@
package org.amnezia.awg
object GoBackend {
external fun awgGetConfig(handle: Int): String?
external fun awgGetSocketV4(handle: Int): Int
external fun awgGetSocketV6(handle: Int): Int
external fun awgTurnOff(handle: Int)
external fun awgTurnOn(ifName: String, tunFd: Int, settings: String): Int
external fun awgVersion(): String
}

View File

@@ -4,6 +4,7 @@ import android.content.Context
import android.net.VpnService.Builder
import java.util.TreeMap
import kotlinx.coroutines.flow.MutableStateFlow
import org.amnezia.awg.GoBackend
import org.amnezia.vpn.protocol.Protocol
import org.amnezia.vpn.protocol.ProtocolState
import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
@@ -61,7 +62,7 @@ open class Wireguard : Protocol() {
override val statistics: Statistics
get() {
if (tunnelHandle == -1) return Statistics.EMPTY_STATISTICS
val config = GoBackend.wgGetConfig(tunnelHandle) ?: return Statistics.EMPTY_STATISTICS
val config = GoBackend.awgGetConfig(tunnelHandle) ?: return Statistics.EMPTY_STATISTICS
return Statistics.build {
var optsCount = 0
config.splitToSequence("\n").forEach { line ->
@@ -92,12 +93,12 @@ open class Wireguard : Protocol() {
val configDataJson = config.getJSONObject("wireguard_config_data")
val configData = parseConfigData(configDataJson.getString("config"))
return WireguardConfig.build {
configWireguard(configData)
configWireguard(configData, configDataJson)
configSplitTunneling(config)
}
}
protected fun WireguardConfig.Builder.configWireguard(configData: Map<String, String>) {
protected fun WireguardConfig.Builder.configWireguard(configData: Map<String, String>, configDataJson: JSONObject) {
configData["Address"]?.split(",")?.map { address ->
InetNetwork.parse(address.trim())
}?.forEach(::addAddress)
@@ -118,7 +119,14 @@ open class Wireguard : Protocol() {
if (routes.any { it !in defRoutes }) disableSplitTunneling()
addRoutes(routes)
configData["MTU"]?.let { setMtu(it.toInt()) }
configDataJson.optString("mtu").let { mtu ->
if (mtu.isNotEmpty()) {
setMtu(mtu.toInt())
} else {
configData["MTU"]?.let { setMtu(it.toInt()) }
}
}
configData["Endpoint"]?.let { setEndpoint(InetEndpoint.parse(it)) }
configData["PersistentKeepalive"]?.let { setPersistentKeepalive(it.toInt()) }
configData["PrivateKey"]?.let { setPrivateKeyHex(it.base64ToHex()) }
@@ -149,8 +157,8 @@ open class Wireguard : Protocol() {
if (tunFd == null) {
throw VpnStartException("Create VPN interface: permission not granted or revoked")
}
Log.v(TAG, "Wg-go backend ${GoBackend.wgVersion()}")
tunnelHandle = GoBackend.wgTurnOn(ifName, tunFd.detachFd(), config.toWgUserspaceString())
Log.v(TAG, "Wg-go backend ${GoBackend.awgVersion()}")
tunnelHandle = GoBackend.awgTurnOn(ifName, tunFd.detachFd(), config.toWgUserspaceString())
}
if (tunnelHandle < 0) {
@@ -158,8 +166,8 @@ open class Wireguard : Protocol() {
throw VpnStartException("Wireguard tunnel creation error")
}
if (!protect(GoBackend.wgGetSocketV4(tunnelHandle)) || !protect(GoBackend.wgGetSocketV6(tunnelHandle))) {
GoBackend.wgTurnOff(tunnelHandle)
if (!protect(GoBackend.awgGetSocketV4(tunnelHandle)) || !protect(GoBackend.awgGetSocketV6(tunnelHandle))) {
GoBackend.awgTurnOff(tunnelHandle)
tunnelHandle = -1
throw VpnStartException("Protect VPN interface: permission not granted or revoked")
}
@@ -172,7 +180,7 @@ open class Wireguard : Protocol() {
}
val handleToClose = tunnelHandle
tunnelHandle = -1
GoBackend.wgTurnOff(handleToClose)
GoBackend.awgTurnOff(handleToClose)
state.value = DISCONNECTED
}

View File

@@ -45,13 +45,12 @@ foreach(abi IN ITEMS ${QT_ANDROID_ABIS})
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/amneziawg/android/${abi}/libwg.so
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/amneziawg/android/${abi}/libwg-go.so
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/amneziawg/android/${abi}/libwg-quick.so
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/shadowsocks/android/${abi}/libredsocks.so
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/shadowsocks/android/${abi}/libsslocal.so
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/shadowsocks/android/${abi}/libtun2socks.so
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/android/${abi}/libck-ovpn-plugin.so
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/android/${abi}/libovpn3.so
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/android/${abi}/libovpnutil.so
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/android/${abi}/librsapss.so
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/libssh/android/${abi}/libssh.so
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openssl3/android/${abi}/libcrypto_3.so
${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openssl3/android/${abi}/libssl_3.so
)
endforeach()

View File

@@ -107,6 +107,7 @@ target_sources(${PROJECT} PRIVATE
${CLIENT_ROOT_DIR}/platforms/ios/LogController.swift
${CLIENT_ROOT_DIR}/platforms/ios/Log.swift
${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift
${CLIENT_ROOT_DIR}/platforms/ios/ScreenProtection.swift
)
target_sources(${PROJECT} PRIVATE

View File

@@ -41,6 +41,8 @@ QString AwgConfigurator::genAwgConfig(const ServerCredentials &credentials, Dock
jsonConfig[config_key::responsePacketMagicHeader] = configMap.value(config_key::responsePacketMagicHeader);
jsonConfig[config_key::underloadPacketMagicHeader] = configMap.value(config_key::underloadPacketMagicHeader);
jsonConfig[config_key::transportPacketMagicHeader] = configMap.value(config_key::transportPacketMagicHeader);
jsonConfig[config_key::mtu] = containerConfig.value(ProtocolProps::protoToString(Proto::Awg)).toObject().
value(config_key::mtu).toString(protocols::awg::defaultMtu);
return QJsonDocument(jsonConfig).toJson();
}

View File

@@ -76,7 +76,7 @@ OpenVpnConfigurator::ConnectionData OpenVpnConfigurator::prepareOpenVpnConfig(co
if (connData.caCert.isEmpty() || connData.clientCert.isEmpty() || connData.taKey.isEmpty()) {
if (errorCode)
*errorCode = ErrorCode::SshSftpFailureError;
*errorCode = ErrorCode::SshScpFailureError;
}
return connData;

View File

@@ -159,7 +159,7 @@ WireguardConfigurator::ConnectionData WireguardConfigurator::prepareWireguardCon
.arg(connData.clientPubKey, connData.pskKey, connData.clientIP);
e = serverController.uploadTextFileToContainer(container, credentials, configPart, m_serverConfigPath,
libssh::SftpOverwriteMode::SftpAppendToExisting);
libssh::ScpOverwriteMode::ScpAppendToExisting);
if (e) {
if (errorCode)
@@ -194,6 +194,7 @@ QString WireguardConfigurator::genWireguardConfig(const ServerCredentials &crede
config.replace("$WIREGUARD_SERVER_PUBLIC_KEY", connData.serverPubKey);
config.replace("$WIREGUARD_PSK", connData.pskKey);
const QJsonObject &wireguarConfig = containerConfig.value(ProtocolProps::protoToString(Proto::WireGuard)).toObject();
QJsonObject jConfig;
jConfig[config_key::config] = config;
@@ -205,6 +206,8 @@ QString WireguardConfigurator::genWireguardConfig(const ServerCredentials &crede
jConfig[config_key::psk_key] = connData.pskKey;
jConfig[config_key::server_pub_key] = connData.serverPubKey;
jConfig[config_key::mtu] = wireguarConfig.value(config_key::mtu).toString(protocols::wireguard::defaultMtu);
clientId = connData.clientPubKey;
return QJsonDocument(jConfig).toJson();

View File

@@ -118,7 +118,7 @@ ServerController::runContainerScript(const ServerCredentials &credentials, Docke
ErrorCode ServerController::uploadTextFileToContainer(DockerContainer container, const ServerCredentials &credentials,
const QString &file, const QString &path,
libssh::SftpOverwriteMode overwriteMode)
libssh::ScpOverwriteMode overwriteMode)
{
ErrorCode e = ErrorCode::NoError;
QString tmpFileName = QString("/tmp/%1.tmp").arg(Utils::getRandomString(16));
@@ -139,7 +139,7 @@ ErrorCode ServerController::uploadTextFileToContainer(DockerContainer container,
if (e)
return e;
if (overwriteMode == libssh::SftpOverwriteMode::SftpOverwriteExisting) {
if (overwriteMode == libssh::ScpOverwriteMode::ScpOverwriteExisting) {
e = runScript(credentials,
replaceVars(QString("sudo docker cp %1 $CONTAINER_NAME:/%2").arg(tmpFileName).arg(path),
genVarsForScript(credentials, container)),
@@ -147,7 +147,7 @@ ErrorCode ServerController::uploadTextFileToContainer(DockerContainer container,
if (e)
return e;
} else if (overwriteMode == libssh::SftpOverwriteMode::SftpAppendToExisting) {
} else if (overwriteMode == libssh::ScpOverwriteMode::ScpAppendToExisting) {
e = runScript(credentials,
replaceVars(QString("sudo docker cp %1 $CONTAINER_NAME:/%2").arg(tmpFileName).arg(tmpFileName),
genVarsForScript(credentials, container)),
@@ -199,7 +199,7 @@ QByteArray ServerController::getTextFileFromContainer(DockerContainer container,
}
ErrorCode ServerController::uploadFileToHost(const ServerCredentials &credentials, const QByteArray &data,
const QString &remotePath, libssh::SftpOverwriteMode overwriteMode)
const QString &remotePath, libssh::ScpOverwriteMode overwriteMode)
{
auto error = m_sshClient.connectToHost(credentials);
if (error != ErrorCode::NoError) {
@@ -211,13 +211,7 @@ ErrorCode ServerController::uploadFileToHost(const ServerCredentials &credential
localFile.write(data);
localFile.close();
#ifdef Q_OS_WINDOWS
error = m_sshClient.sftpFileCopy(overwriteMode, localFile.fileName().toLocal8Bit().toStdString(), remotePath.toStdString(),
"non_desc");
#else
error = m_sshClient.sftpFileCopy(overwriteMode, localFile.fileName().toStdString(), remotePath.toStdString(),
"non_desc");
#endif
error = m_sshClient.scpFileCopy(overwriteMode, localFile.fileName(), remotePath, "non_desc");
if (error != ErrorCode::NoError) {
return error;
@@ -365,7 +359,33 @@ bool ServerController::isReinstallContainerRequired(DockerContainer container, c
}
if (container == DockerContainer::Awg) {
return true;
if ((oldProtoConfig.value(config_key::port).toString(protocols::awg::defaultPort)
!= newProtoConfig.value(config_key::port).toString(protocols::awg::defaultPort))
|| (oldProtoConfig.value(config_key::junkPacketCount).toString(protocols::awg::defaultJunkPacketCount)
!= newProtoConfig.value(config_key::junkPacketCount).toString(protocols::awg::defaultJunkPacketCount))
|| (oldProtoConfig.value(config_key::junkPacketMinSize).toString(protocols::awg::defaultJunkPacketMinSize)
!= newProtoConfig.value(config_key::junkPacketMinSize).toString(protocols::awg::defaultJunkPacketMinSize))
|| (oldProtoConfig.value(config_key::junkPacketMaxSize).toString(protocols::awg::defaultJunkPacketMaxSize)
!= newProtoConfig.value(config_key::junkPacketMaxSize).toString(protocols::awg::defaultJunkPacketMaxSize))
|| (oldProtoConfig.value(config_key::initPacketJunkSize).toString(protocols::awg::defaultInitPacketJunkSize)
!= newProtoConfig.value(config_key::initPacketJunkSize).toString(protocols::awg::defaultInitPacketJunkSize))
|| (oldProtoConfig.value(config_key::responsePacketJunkSize).toString(protocols::awg::defaultResponsePacketJunkSize)
!= newProtoConfig.value(config_key::responsePacketJunkSize).toString(protocols::awg::defaultResponsePacketJunkSize))
|| (oldProtoConfig.value(config_key::initPacketMagicHeader).toString(protocols::awg::defaultInitPacketMagicHeader)
!= newProtoConfig.value(config_key::initPacketMagicHeader).toString(protocols::awg::defaultInitPacketMagicHeader))
|| (oldProtoConfig.value(config_key::responsePacketMagicHeader).toString(protocols::awg::defaultResponsePacketMagicHeader)
!= newProtoConfig.value(config_key::responsePacketMagicHeader).toString(protocols::awg::defaultResponsePacketMagicHeader))
|| (oldProtoConfig.value(config_key::underloadPacketMagicHeader).toString(protocols::awg::defaultUnderloadPacketMagicHeader)
!= newProtoConfig.value(config_key::underloadPacketMagicHeader).toString(protocols::awg::defaultUnderloadPacketMagicHeader))
|| (oldProtoConfig.value(config_key::transportPacketMagicHeader).toString(protocols::awg::defaultTransportPacketMagicHeader)
!= newProtoConfig.value(config_key::transportPacketMagicHeader).toString(protocols::awg::defaultTransportPacketMagicHeader)))
return true;
}
if (container == DockerContainer::WireGuard){
if (oldProtoConfig.value(config_key::port).toString(protocols::wireguard::defaultPort)
!= newProtoConfig.value(config_key::port).toString(protocols::wireguard::defaultPort))
return true;
}
return false;

View File

@@ -38,7 +38,7 @@ public:
ErrorCode uploadTextFileToContainer(
DockerContainer container, const ServerCredentials &credentials, const QString &file, const QString &path,
libssh::SftpOverwriteMode overwriteMode = libssh::SftpOverwriteMode::SftpOverwriteExisting);
libssh::ScpOverwriteMode overwriteMode = libssh::ScpOverwriteMode::ScpOverwriteExisting);
QByteArray getTextFileFromContainer(DockerContainer container, const ServerCredentials &credentials,
const QString &path, ErrorCode *errorCode = nullptr);
@@ -80,7 +80,7 @@ private:
ErrorCode isServerDpkgBusy(const ServerCredentials &credentials, DockerContainer container);
ErrorCode uploadFileToHost(const ServerCredentials &credentials, const QByteArray &data, const QString &remotePath,
libssh::SftpOverwriteMode overwriteMode = libssh::SftpOverwriteMode::SftpOverwriteExisting);
libssh::ScpOverwriteMode overwriteMode = libssh::ScpOverwriteMode::ScpOverwriteExisting);
ErrorCode setupServerFirewall(const ServerCredentials &credentials);

View File

@@ -46,25 +46,12 @@ namespace amnezia
SshPrivateKeyFormatError = 304,
SshTimeoutError = 305,
// Ssh sftp errors
SshSftpEofError = 400,
SshSftpNoSuchFileError = 401,
SshSftpPermissionDeniedError = 402,
SshSftpFailureError = 403,
SshSftpBadMessageError = 404,
SshSftpNoConnectionError = 405,
SshSftpConnectionLostError = 406,
SshSftpOpUnsupportedError = 407,
SshSftpInvalidHandleError = 408,
SshSftpNoSuchPathError = 409,
SshSftpFileAlreadyExistsError = 410,
SshSftpWriteProtectError = 411,
SshSftpNoMediaError = 412,
// Ssh scp errors
SshScpFailureError = 400,
// Local errors
OpenVpnConfigMissing = 500,
OpenVpnManagementServerError = 501,
ConfigMissing = 502,
// Distro errors
OpenVpnExecutableMissing = 600,
@@ -92,7 +79,15 @@ namespace amnezia
// Api errors
ApiConfigDownloadError = 1100,
ApiConfigAlreadyAdded = 1101
ApiConfigAlreadyAdded = 1101,
// QFile errors
OpenError = 1200,
ReadError = 1201,
PermissionsError = 1202,
UnspecifiedError = 1203,
FatalError = 1204,
AbortError = 1205
};
} // namespace amnezia

View File

@@ -28,20 +28,8 @@ QString errorString(ErrorCode code) {
case(SshPrivateKeyFormatError): errorMessage = QObject::tr("The selected private key format is not supported, use openssh ED25519 key types or PEM key types"); break;
case(SshTimeoutError): errorMessage = QObject::tr("Timeout connecting to server"); break;
// Libssh sftp errors
case(SshSftpEofError): errorMessage = QObject::tr("Sftp error: End-of-file encountered"); break;
case(SshSftpNoSuchFileError): errorMessage = QObject::tr("Sftp error: File does not exist"); break;
case(SshSftpPermissionDeniedError): errorMessage = QObject::tr("Sftp error: Permission denied"); break;
case(SshSftpFailureError): errorMessage = QObject::tr("Sftp error: Generic failure"); break;
case(SshSftpBadMessageError): errorMessage = QObject::tr("Sftp error: Garbage received from server"); break;
case(SshSftpNoConnectionError): errorMessage = QObject::tr("Sftp error: No connection has been set up"); break;
case(SshSftpConnectionLostError): errorMessage = QObject::tr("Sftp error: There was a connection, but we lost it"); break;
case(SshSftpOpUnsupportedError): errorMessage = QObject::tr("Sftp error: Operation not supported by libssh yet"); break;
case(SshSftpInvalidHandleError): errorMessage = QObject::tr("Sftp error: Invalid file handle"); break;
case(SshSftpNoSuchPathError): errorMessage = QObject::tr("Sftp error: No such file or directory path exists"); break;
case(SshSftpFileAlreadyExistsError): errorMessage = QObject::tr("Sftp error: An attempt to create an already existing file or directory has been made"); break;
case(SshSftpWriteProtectError): errorMessage = QObject::tr("Sftp error: Write-protected filesystem"); break;
case(SshSftpNoMediaError): errorMessage = QObject::tr("Sftp error: No media was in remote drive"); break;
// Ssh scp errors
case(SshScpFailureError): errorMessage = QObject::tr("Scp error: Generic failure"); break;
// Local errors
case (OpenVpnConfigMissing): errorMessage = QObject::tr("OpenVPN config missing"); break;
@@ -68,6 +56,14 @@ QString errorString(ErrorCode code) {
case (ApiConfigDownloadError): errorMessage = QObject::tr("Error when retrieving configuration from API"); break;
case (ApiConfigAlreadyAdded): errorMessage = QObject::tr("This config has already been added to the application"); break;
// QFile errors
case(OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
case(ReadError): errorMessage = QObject::tr("QFile error: An error occurred when reading from the file"); break;
case(PermissionsError): errorMessage = QObject::tr("QFile error: The file could not be accessed"); break;
case(UnspecifiedError): errorMessage = QObject::tr("QFile error: An unspecified error occurred"); break;
case(FatalError): errorMessage = QObject::tr("QFile error: A fatal error occurred"); break;
case(AbortError): errorMessage = QObject::tr("QFile error: The operation was aborted"); break;
case(InternalError):
default:
errorMessage = QObject::tr("Internal error"); break;

View File

@@ -71,7 +71,7 @@ QSharedPointer<PrivilegedProcess> IpcClient::CreatePrivilegedProcess()
}
QRemoteObjectPendingReply<int> futureResult = Instance()->m_ipcClient->createPrivilegedProcess();
futureResult.waitForFinished(1000);
futureResult.waitForFinished(5000);
int pid = futureResult.returnValue();

View File

@@ -10,16 +10,10 @@ const uint32_t S_IRWXU = 0644;
#endif
namespace libssh {
const QString libsshTimeoutError = "Timeout connecting to";
constexpr auto libsshTimeoutError{"Timeout connecting to"};
std::function<QString()> Client::m_passphraseCallback;
Client::Client(QObject *parent) : QObject(parent)
{ }
Client::~Client()
{ }
int Client::callback(const char *prompt, char *buf, size_t len, int echo, int verify, void *userdata)
{
auto passphrase = m_passphraseCallback();
@@ -171,13 +165,13 @@ namespace libssh {
return ErrorCode::NoError;
};
auto error = readOutput(false);
if (error != ErrorCode::NoError) {
return error;
auto errorCode = readOutput(false);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
error = readOutput(true);
if (error != ErrorCode::NoError) {
return error;
errorCode = readOutput(true);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
} else {
return closeChannel();
@@ -222,102 +216,79 @@ namespace libssh {
return fromLibsshErrorCode();
}
ErrorCode Client::sftpFileCopy(const SftpOverwriteMode overwriteMode, const std::string& localPath, const std::string& remotePath, const std::string& fileDesc)
ErrorCode Client::scpFileCopy(const ScpOverwriteMode overwriteMode, const QString& localPath, const QString& remotePath, const QString &fileDesc)
{
m_sftpSession = sftp_new(m_session);
m_scpSession = ssh_scp_new(m_session, SSH_SCP_WRITE, remotePath.toStdString().c_str());
if (m_sftpSession == nullptr) {
return closeSftpSession();
if (m_scpSession == nullptr) {
return fromLibsshErrorCode();
}
int result = sftp_init(m_sftpSession);
if (result != SSH_OK) {
return closeSftpSession();
if (ssh_scp_init(m_scpSession) != SSH_OK) {
auto errorCode = fromLibsshErrorCode();
closeScpSession();
return errorCode;
}
QFutureWatcher<ErrorCode> watcher;
connect(&watcher, &QFutureWatcher<ErrorCode>::finished, this, &Client::sftpFileCopyFinished);
connect(&watcher, &QFutureWatcher<ErrorCode>::finished, this, &Client::scpFileCopyFinished);
QFuture<ErrorCode> future = QtConcurrent::run([this, overwriteMode, &localPath, &remotePath, &fileDesc]() {
int accessType = O_WRONLY | O_CREAT | overwriteMode;
sftp_file file;
const size_t bufferSize = 16384;
char buffer[bufferSize];
const int accessType = O_WRONLY | O_CREAT | overwriteMode;
const int localFileSize = QFileInfo(localPath).size();
file = sftp_open(m_sftpSession, remotePath.c_str(), accessType, S_IRWXU);
if (file == nullptr) {
return closeSftpSession();
int result = ssh_scp_push_file(m_scpSession, remotePath.toStdString().c_str(), localFileSize, accessType);
if (result != SSH_OK) {
return fromLibsshErrorCode();
}
int localFileSize = std::filesystem::file_size(localPath);
int chunksCount = localFileSize / (bufferSize);
QFile fin(localPath);
std::ifstream fin(localPath, std::ios::binary | std::ios::in);
if (fin.open(QIODevice::ReadOnly)) {
constexpr size_t bufferSize = 16384;
int transferred = 0;
int currentChunkSize = bufferSize;
if (fin.is_open()) {
for (int currentChunkId = 0; currentChunkId < chunksCount; currentChunkId++) {
fin.read(buffer, bufferSize);
while (transferred < localFileSize) {
int bytesWritten = sftp_write(file, buffer, bufferSize);
std::string chunk(buffer, bufferSize);
if (bytesWritten != bufferSize) {
fin.close();
sftp_close(file);
return closeSftpSession();
// Last Chunk
if ((localFileSize - transferred) < bufferSize) {
currentChunkSize = localFileSize % bufferSize;
}
}
int lastChunkSize = localFileSize % (bufferSize);
if (lastChunkSize != 0) {
fin.read(buffer, lastChunkSize);
std::string chunk(buffer, lastChunkSize);
int bytesWritten = sftp_write(file, buffer, lastChunkSize);
if (bytesWritten != lastChunkSize) {
fin.close();
sftp_close(file);
return closeSftpSession();
QByteArray chunk = fin.read(currentChunkSize);
if (chunk.size() != currentChunkSize) {
return fromFileErrorCode(fin.error());
}
result = ssh_scp_write(m_scpSession, chunk.data(), chunk.size());
if (result != SSH_OK) {
return fromLibsshErrorCode();
}
transferred += currentChunkSize;
}
} else {
sftp_close(file);
return closeSftpSession();
return fromFileErrorCode(fin.error());
}
fin.close();
int result = sftp_close(file);
if (result != SSH_OK) {
return closeSftpSession();
}
return closeSftpSession();
return ErrorCode::NoError;
});
watcher.setFuture(future);
QEventLoop wait;
QObject::connect(this, &Client::sftpFileCopyFinished, &wait, &QEventLoop::quit);
QObject::connect(this, &Client::scpFileCopyFinished, &wait, &QEventLoop::quit);
wait.exec();
closeScpSession();
return watcher.result();
}
ErrorCode Client::closeSftpSession()
void Client::closeScpSession()
{
auto errorCode = fromLibsshSftpErrorCode(sftp_get_error(m_sftpSession));
if (m_sftpSession != nullptr) {
sftp_free(m_sftpSession);
m_sftpSession = nullptr;
if (m_scpSession != nullptr) {
ssh_scp_free(m_scpSession);
m_scpSession = nullptr;
}
qCritical() << ssh_get_error(m_session);
return errorCode;
}
ErrorCode Client::fromLibsshErrorCode()
@@ -339,24 +310,17 @@ namespace libssh {
default: return ErrorCode::SshInternalError;
}
}
ErrorCode Client::fromLibsshSftpErrorCode(int errorCode)
ErrorCode Client::fromFileErrorCode(QFileDevice::FileError fileError)
{
switch (errorCode) {
case(SSH_FX_OK): return ErrorCode::NoError;
case(SSH_FX_EOF): return ErrorCode::SshSftpEofError;
case(SSH_FX_NO_SUCH_FILE): return ErrorCode::SshSftpNoSuchFileError;
case(SSH_FX_PERMISSION_DENIED): return ErrorCode::SshSftpPermissionDeniedError;
case(SSH_FX_FAILURE): return ErrorCode::SshSftpFailureError;
case(SSH_FX_BAD_MESSAGE): return ErrorCode::SshSftpBadMessageError;
case(SSH_FX_NO_CONNECTION): return ErrorCode::SshSftpNoConnectionError;
case(SSH_FX_CONNECTION_LOST): return ErrorCode::SshSftpConnectionLostError;
case(SSH_FX_OP_UNSUPPORTED): return ErrorCode::SshSftpOpUnsupportedError;
case(SSH_FX_INVALID_HANDLE): return ErrorCode::SshSftpInvalidHandleError;
case(SSH_FX_NO_SUCH_PATH): return ErrorCode::SshSftpNoSuchPathError;
case(SSH_FX_FILE_ALREADY_EXISTS): return ErrorCode::SshSftpFileAlreadyExistsError;
case(SSH_FX_WRITE_PROTECT): return ErrorCode::SshSftpWriteProtectError;
case(SSH_FX_NO_MEDIA): return ErrorCode::SshSftpNoMediaError;
default: return ErrorCode::SshSftpFailureError;
switch (fileError) {
case QFileDevice::NoError: return ErrorCode::NoError;
case QFileDevice::ReadError: return ErrorCode::ReadError;
case QFileDevice::OpenError: return ErrorCode::OpenError;
case QFileDevice::PermissionsError: return ErrorCode::PermissionsError;
case QFileDevice::FatalError: return ErrorCode::FatalError;
case QFileDevice::AbortError: return ErrorCode::AbortError;
default: return ErrorCode::UnspecifiedError;
}
}

View File

@@ -2,29 +2,29 @@
#define SSHCLIENT_H
#include <QObject>
#include <QFile>
#include <fcntl.h>
#include <libssh/libssh.h>
#include <libssh/sftp.h>
#include "defs.h"
using namespace amnezia;
namespace libssh {
enum SftpOverwriteMode {
enum ScpOverwriteMode {
/*! Overwrite any existing files */
SftpOverwriteExisting = O_TRUNC,
ScpOverwriteExisting = O_TRUNC,
/*! Append new content if the file already exists */
SftpAppendToExisting = O_APPEND
ScpAppendToExisting = O_APPEND
};
class Client : public QObject
{
Q_OBJECT
public:
Client(QObject *parent = nullptr);
~Client();
Client() = default;
~Client() = default;
ErrorCode connectToHost(const ServerCredentials &credentials);
void disconnectFromHost();
@@ -32,26 +32,26 @@ namespace libssh {
const std::function<ErrorCode (const QString &, Client &)> &cbReadStdOut,
const std::function<ErrorCode (const QString &, Client &)> &cbReadStdErr);
ErrorCode writeResponse(const QString &data);
ErrorCode sftpFileCopy(const SftpOverwriteMode overwriteMode,
const std::string& localPath,
const std::string& remotePath,
const std::string& fileDesc);
ErrorCode scpFileCopy(const ScpOverwriteMode overwriteMode,
const QString &localPath,
const QString &remotePath,
const QString &fileDesc);
ErrorCode getDecryptedPrivateKey(const ServerCredentials &credentials, QString &decryptedPrivateKey, const std::function<QString()> &passphraseCallback);
private:
ErrorCode closeChannel();
ErrorCode closeSftpSession();
void closeScpSession();
ErrorCode fromLibsshErrorCode();
ErrorCode fromLibsshSftpErrorCode(int errorCode);
ErrorCode fromFileErrorCode(QFileDevice::FileError fileError);
static int callback(const char *prompt, char *buf, size_t len, int echo, int verify, void *userdata);
ssh_session m_session = nullptr;
ssh_channel m_channel = nullptr;
sftp_session m_sftpSession = nullptr;
ssh_scp m_scpSession = nullptr;
static std::function<QString()> m_passphraseCallback;
signals:
void writeToChannelFinished();
void sftpFileCopyFinished();
void scpFileCopyFinished();
};
}

View File

@@ -251,6 +251,19 @@ bool Daemon::parseConfig(const QJsonObject& obj, InterfaceConfig& config) {
GETVALUE("serverPskKey", config.m_serverPskKey, String);
GETVALUE("serverPort", config.m_serverPort, Double);
if (!obj.contains("deviceMTU") || obj.value("deviceMTU").toString().toInt() == 0)
{
config.m_deviceMTU = 1420;
} else {
config.m_deviceMTU = obj.value("deviceMTU").toString().toInt();
#ifdef Q_OS_WINDOWS
// For Windows min MTU value is 1280 (the smallest MTU legal with IPv6).
if (config.m_deviceMTU < 1280) {
config.m_deviceMTU = 1280;
}
#endif
}
config.m_deviceIpv4Address = obj.value("deviceIpv4Address").toString();
config.m_deviceIpv6Address = obj.value("deviceIpv6Address").toString();
if (config.m_deviceIpv4Address.isNull() &&

View File

@@ -23,6 +23,7 @@ QJsonObject InterfaceConfig::toJson() const {
json.insert("serverIpv4AddrIn", QJsonValue(m_serverIpv4AddrIn));
json.insert("serverIpv6AddrIn", QJsonValue(m_serverIpv6AddrIn));
json.insert("serverPort", QJsonValue((double)m_serverPort));
json.insert("deviceMTU", QJsonValue(m_deviceMTU));
if ((m_hopType == InterfaceConfig::MultiHopExit) ||
(m_hopType == InterfaceConfig::SingleHop)) {
json.insert("serverIpv4Gateway", QJsonValue(m_serverIpv4Gateway));
@@ -85,8 +86,13 @@ QString InterfaceConfig::toWgConf(const QMap<QString, QString>& extra) const {
if (addresses.isEmpty()) {
return "";
}
out << "Address = " << addresses.join(", ") << "\n";
if (m_deviceMTU) {
out << "MTU = " << m_deviceMTU << "\n";
}
if (!m_dnsServer.isNull()) {
QStringList dnsServers(m_dnsServer);
// If the DNS is not the Gateway, it's a user defined DNS

View File

@@ -33,6 +33,7 @@ class InterfaceConfig {
QString m_serverIpv6AddrIn;
QString m_dnsServer;
int m_serverPort = 0;
int m_deviceMTU = 1420;
QList<IPAddress> m_allowedIPAddressRanges;
QStringList m_excludedAddresses;
QStringList m_vpnDisabledApps;

View File

@@ -0,0 +1,6 @@
<svg width="19" height="18" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" width="18" height="18" rx="5" fill="white"/>
<path d="M8.49219 13.5L8.49219 9.44141L14.0191 4.99484" stroke="#0E0E11" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.47363 5.49805L6.98828 8.0127" stroke="#0E0E11" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.4727 9.5L14.4727 4.5033L9.50195 4.5033" stroke="#0E0E11" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 511 B

View File

@@ -84,7 +84,9 @@ target_sources(networkextension PRIVATE
${CLIENT_ROOT_DIR}/platforms/ios/Log.swift
${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift
${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider.swift
${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider+OpenVPNAdapterDelegate.swift
${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider+WireGuard.swift
${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider+OpenVPN.swift
${CLIENT_ROOT_DIR}/platforms/ios/WGConfig.swift
${CLIENT_ROOT_DIR}/platforms/ios/iosglue.mm
)

View File

@@ -26,9 +26,10 @@ int main(int argc, char *argv[])
AllowSetForegroundWindow(ASFW_ANY);
#endif
// QTBUG-95974 QTBUG-95764 QTBUG-102168
#ifdef Q_OS_ANDROID
// QTBUG-95974 QTBUG-95764 QTBUG-102168
qputenv("QT_ANDROID_DISABLE_ACCESSIBILITY", "1");
qputenv("ANDROID_OPENSSL_SUFFIX", "_3");
#endif
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
@@ -48,10 +49,6 @@ int main(int argc, char *argv[])
AllowSetForegroundWindow(0);
#endif
#if defined(Q_OS_IOS)
QtAppDelegateInitialize();
#endif
app.registerTypes();
app.setApplicationName(APPLICATION_NAME);
@@ -65,7 +62,7 @@ int main(int argc, char *argv[])
if (doExec) {
app.init();
qInfo().noquote() << QString("Started %1 version %2").arg(APPLICATION_NAME, APP_VERSION);
qInfo().noquote() << QString("Started %1 version %2 %3").arg(APPLICATION_NAME, APP_VERSION, GIT_COMMIT_HASH);
qInfo().noquote() << QString("%1 (%2)").arg(QSysInfo::prettyProductName(), QSysInfo::currentCpuArchitecture());
return app.exec();

View File

@@ -124,13 +124,21 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
// json.insert("hopindex", QJsonValue((double)hop.m_hopindex));
json.insert("privateKey", wgConfig.value(amnezia::config_key::client_priv_key));
json.insert("deviceIpv4Address", wgConfig.value(amnezia::config_key::client_ip));
json.insert("deviceIpv6Address", "dead::1");
// set up IPv6 unique-local-address, ULA, with "fd00::/8" prefix, not globally routable.
// this will be default IPv6 gateway, OS recognizes that IPv6 link is local and switches to IPv4.
// Otherwise some OSes (Linux) try IPv6 forever and hang.
// https://en.wikipedia.org/wiki/Unique_local_address (RFC 4193)
// https://man7.org/linux/man-pages/man5/gai.conf.5.html
json.insert("deviceIpv6Address", "fd58:baa6:dead::1"); // simply "dead::1" is globally-routable, don't use it
json.insert("serverPublicKey", wgConfig.value(amnezia::config_key::server_pub_key));
json.insert("serverPskKey", wgConfig.value(amnezia::config_key::psk_key));
json.insert("serverIpv4AddrIn", wgConfig.value(amnezia::config_key::hostName));
// json.insert("serverIpv6AddrIn", QJsonValue(hop.m_server.ipv6AddrIn()));
json.insert("serverPort", wgConfig.value(amnezia::config_key::port).toInt());
json.insert("deviceMTU", wgConfig.value(amnezia::config_key::mtu));
json.insert("serverPort", wgConfig.value(amnezia::config_key::port).toInt());
json.insert("serverIpv4Gateway", wgConfig.value(amnezia::config_key::hostName));
// json.insert("serverIpv6Gateway", QJsonValue(hop.m_server.ipv6Gateway()));
json.insert("dnsServer", rawConfig.value(amnezia::config_key::dns1));

View File

@@ -56,26 +56,10 @@ AndroidController::AndroidController() : QObject()
Qt::QueuedConnection);
connect(
this, &AndroidController::vpnConnected, this,
[this]() {
qDebug() << "Android event: VPN connected";
emit connectionStateChanged(Vpn::ConnectionState::Connected);
},
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);
this, &AndroidController::vpnStateChanged, this,
[this](AndroidController::ConnectionState state) {
qDebug() << "Android event: VPN state changed:" << textConnectionState(state);
emit connectionStateChanged(convertState(state));
},
Qt::QueuedConnection);
@@ -106,9 +90,7 @@ bool AndroidController::initialize()
{"onServiceDisconnected", "()V", reinterpret_cast<void *>(onServiceDisconnected)},
{"onServiceError", "()V", reinterpret_cast<void *>(onServiceError)},
{"onVpnPermissionRejected", "()V", reinterpret_cast<void *>(onVpnPermissionRejected)},
{"onVpnConnected", "()V", reinterpret_cast<void *>(onVpnConnected)},
{"onVpnDisconnected", "()V", reinterpret_cast<void *>(onVpnDisconnected)},
{"onVpnReconnecting", "()V", reinterpret_cast<void *>(onVpnReconnecting)},
{"onVpnStateChanged", "(I)V", reinterpret_cast<void *>(onVpnStateChanged)},
{"onStatisticsUpdate", "(JJ)V", reinterpret_cast<void *>(onStatisticsUpdate)},
{"onFileOpened", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onFileOpened)},
{"onConfigImported", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onConfigImported)},
@@ -158,6 +140,11 @@ void AndroidController::stop()
callActivityMethod("stop", "()V");
}
void AndroidController::resetLastServer(int serverIndex)
{
callActivityMethod("resetLastServer", "(I)V", serverIndex);
}
void AndroidController::saveFile(const QString &fileName, const QString &data)
{
callActivityMethod("saveFile", "(Ljava/lang/String;Ljava/lang/String;)V",
@@ -217,6 +204,11 @@ void AndroidController::clearLogs()
callActivityMethod("clearLogs", "()V");
}
void AndroidController::setScreenshotsEnabled(bool enabled)
{
callActivityMethod("setScreenshotsEnabled", "(Z)V", enabled);
}
// Moving log processing to the Android side
jclass AndroidController::log;
jmethodID AndroidController::logDebug;
@@ -370,30 +362,14 @@ void AndroidController::onVpnPermissionRejected(JNIEnv *env, jobject thiz)
}
// static
void AndroidController::onVpnConnected(JNIEnv *env, jobject thiz)
void AndroidController::onVpnStateChanged(JNIEnv *env, jobject thiz, jint stateCode)
{
Q_UNUSED(env);
Q_UNUSED(thiz);
emit AndroidController::instance()->vpnConnected();
}
auto state = ConnectionState(stateCode);
// static
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();
emit AndroidController::instance()->vpnStateChanged(state);
}
// static

View File

@@ -20,9 +20,9 @@ public:
// keep synchronized with org.amnezia.vpn.protocol.ProtocolState
enum class ConnectionState
{
DISCONNECTED,
CONNECTED,
CONNECTING,
DISCONNECTED,
DISCONNECTING,
RECONNECTING,
UNKNOWN
@@ -30,6 +30,7 @@ public:
ErrorCode start(const QJsonObject &vpnConfig);
void stop();
void resetLastServer(int serverIndex);
void setNotificationText(const QString &title, const QString &message, int timerSec);
void saveFile(const QString &fileName, const QString &data);
QString openFile(const QString &filter);
@@ -38,6 +39,7 @@ public:
void setSaveLogs(bool enabled);
void exportLogsFile(const QString &fileName);
void clearLogs();
void setScreenshotsEnabled(bool enabled);
static bool initLogging();
static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message);
@@ -48,9 +50,7 @@ signals:
void serviceDisconnected();
void serviceError();
void vpnPermissionRejected();
void vpnConnected();
void vpnDisconnected();
void vpnReconnecting();
void vpnStateChanged(ConnectionState state);
void statisticsUpdated(quint64 rxBytes, quint64 txBytes);
void fileOpened(QString uri);
void configImported(QString config);
@@ -77,9 +77,7 @@ private:
static void onServiceDisconnected(JNIEnv *env, jobject thiz);
static void onServiceError(JNIEnv *env, jobject thiz);
static void onVpnPermissionRejected(JNIEnv *env, jobject thiz);
static void onVpnConnected(JNIEnv *env, jobject thiz);
static void onVpnDisconnected(JNIEnv *env, jobject thiz);
static void onVpnReconnecting(JNIEnv *env, jobject thiz);
static void onVpnStateChanged(JNIEnv *env, jobject thiz, jint stateCode);
static void onStatisticsUpdate(JNIEnv *env, jobject thiz, jlong rxBytes, jlong txBytes);
static void onConfigImported(JNIEnv *env, jobject thiz, jstring data);
static void onFileOpened(JNIEnv *env, jobject thiz, jstring uri);

View File

@@ -2,6 +2,8 @@ import Foundation
import os.log
struct Log {
static let osLog = Logger()
private static let IsLoggingEnabledKey = "IsLoggingEnabled"
static var isLoggingEnabled: Bool {
get {
@@ -29,16 +31,23 @@ struct Log {
return dateFormatter
}()
var records: [Record]
var records = [Record]()
var lastRecordDate = Date.distantPast
init() {
self.records = []
}
init(_ str: String) {
self.records = str.split(whereSeparator: \.isNewline)
.compactMap {
Record(String($0))!
records = str.split(whereSeparator: \.isNewline)
.map {
if let record = Record(String($0)) {
lastRecordDate = record.date
return record
} else {
return Record(date: lastRecordDate, level: .error, message: "LOG: \($0)")
}
}
}
@@ -60,6 +69,24 @@ struct Log {
self.init(str)
}
static func log(_ type: OSLogType, title: String = "", message: String, url: URL = neLogURL) {
guard isLoggingEnabled else { return }
let date = Date()
let level = Record.Level(from: type)
let messages = message.split(whereSeparator: \.isNewline)
for index in 0..<messages.count {
let message = String(messages[index])
if index != 0 && message.first != " " {
Record(date: date, level: level, message: "\(title) \(message)").save(at: url)
} else {
Record(date: date, level: level, message: "\(title)\(message)").save(at: url)
}
}
}
static func clear(at url: URL) {
if FileManager.default.fileExists(atPath: url.path) {
guard let fileHandle = try? FileHandle(forUpdating: url) else { return }

View File

@@ -1,4 +1,5 @@
import Foundation
import NetworkExtension
public func swiftUpdateLogData(_ qtString: std.string) -> std.string {
let qtLog = Log(String(describing: qtString))
@@ -24,3 +25,26 @@ public func swiftDeleteLog() {
public func toggleLogging(_ isEnabled: Bool) {
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)")
}
}
}
}
}
}
}

View File

@@ -30,6 +30,8 @@ extension Log {
}
func save(at url: URL) {
osLog.log(level: level.osLogType, "\(message)")
guard let data = "\n\(description)".data(using: .utf8) else { return }
if !FileManager.default.fileExists(atPath: url.path) {
@@ -64,19 +66,38 @@ extension Log.Record {
init(from osLogType: OSLogType) {
switch osLogType {
case OSLogType.default:
case .default:
self = .info
case OSLogType.info:
case .info:
self = .info
case OSLogType.debug:
case .debug:
self = .debug
case OSLogType.error:
case .error:
self = .error
case OSLogType.fault:
case .fault:
self = .fatal
default:
self = .info
}
}
var osLogType: OSLogType {
switch self {
case .info:
return .info
case .debug:
return .debug
case .error:
return .error
case .fatal:
return .fault
case .warning:
return .info
case .critical:
return .fault
case .system:
return .fault
}
}
}
}

View File

@@ -1,18 +1,18 @@
import Foundation
import os.log
public func wg_log(_ type: OSLogType, staticMessage: StaticString) {
guard Log.isLoggingEnabled else { return }
Log.Record(date: Date(), level: Log.Record.Level(from: type), message: "\(staticMessage)").save(at: Log.neLogURL)
public func wg_log(_ type: OSLogType, title: String = "", staticMessage: StaticString) {
neLog(type, title: "WG: \(title)", message: "\(staticMessage)")
}
public func wg_log(_ type: OSLogType, message: String) {
log(type, message: message)
public func wg_log(_ type: OSLogType, title: String = "", message: String) {
neLog(type, title: "WG: \(title)", message: message)
}
public func log(_ type: OSLogType, message: String) {
guard Log.isLoggingEnabled else { return }
Log.Record(date: Date(), level: Log.Record.Level(from: type), message: message).save(at: Log.neLogURL)
public func ovpnLog(_ type: OSLogType, title: String = "", message: String) {
neLog(type, title: "OVPN: \(title)", message: message)
}
public func neLog(_ type: OSLogType, title: String = "", message: String) {
Log.log(type, title: "NE: \(title)", message: message)
}

View File

@@ -0,0 +1,223 @@
import Foundation
import NetworkExtension
import OpenVPNAdapter
struct OpenVPNConfig: Decodable {
let config: String
let splitTunnelType: Int
let splitTunnelSites: [String]
var str: String {
"splitTunnelType: \(splitTunnelType) splitTunnelSites: \(splitTunnelSites) config: \(config)"
}
}
extension PacketTunnelProvider {
func startOpenVPN(completionHandler: @escaping (Error?) -> Void) {
guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol,
let providerConfiguration = protocolConfiguration.providerConfiguration,
let openVPNConfigData = providerConfiguration[Constants.ovpnConfigKey] as? Data else {
ovpnLog(.error, message: "Can't start")
return
}
do {
// ovpnLog(.info, message: "providerConfiguration: \(String(decoding: openVPNConfigData, as: UTF8.self))")
let openVPNConfig = try JSONDecoder().decode(OpenVPNConfig.self, from: openVPNConfigData)
ovpnLog(.info, title: "config: ", message: openVPNConfig.str)
let ovpnConfiguration = Data(openVPNConfig.config.utf8)
setupAndlaunchOpenVPN(withConfig: ovpnConfiguration, completionHandler: completionHandler)
} catch {
ovpnLog(.error, message: "Can't parse config: \(error.localizedDescription)")
if let underlyingError = (error as NSError).userInfo[NSUnderlyingErrorKey] as? NSError {
ovpnLog(.error, message: "Can't parse config: \(underlyingError.localizedDescription)")
}
return
}
}
private func setupAndlaunchOpenVPN(withConfig ovpnConfiguration: Data,
withShadowSocks viaSS: Bool = false,
completionHandler: @escaping (Error?) -> Void) {
ovpnLog(.info, message: "Setup and launch")
let str = String(decoding: ovpnConfiguration, as: UTF8.self)
let configuration = OpenVPNConfiguration()
configuration.fileContent = ovpnConfiguration
if str.contains("cloak") {
configuration.setPTCloak()
}
let evaluation: OpenVPNConfigurationEvaluation
do {
evaluation = try ovpnAdapter.apply(configuration: configuration)
} catch {
completionHandler(error)
return
}
if !evaluation.autologin {
ovpnLog(.info, message: "Implement login with user credentials")
}
vpnReachability.startTracking { [weak self] status in
guard status == .reachableViaWiFi else { return }
self?.ovpnAdapter.reconnect(afterTimeInterval: 5)
}
startHandler = completionHandler
ovpnAdapter.connect(using: packetFlow)
// let ifaces = Interface.allInterfaces()
// .filter { $0.family == .ipv4 }
// .map { iface in iface.name }
// ovpn_log(.error, message: "Available TUN Interfaces: \(ifaces)")
}
func handleOpenVPNStatusMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
guard let completionHandler = completionHandler else { return }
let bytesin = ovpnAdapter.transportStatistics.bytesIn
let bytesout = ovpnAdapter.transportStatistics.bytesOut
let response: [String: Any] = [
"rx_bytes": bytesin,
"tx_bytes": bytesout
]
completionHandler(try? JSONSerialization.data(withJSONObject: response, options: []))
}
func stopOpenVPN(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
stopHandler = completionHandler
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
ovpnAdapter.disconnect()
}
}
extension PacketTunnelProvider: OpenVPNAdapterDelegate {
// OpenVPNAdapter calls this delegate method to configure a VPN tunnel.
// `completionHandler` callback requires an object conforming to `OpenVPNAdapterPacketFlow`
// protocol if the tunnel is configured without errors. Otherwise send nil.
// `OpenVPNAdapterPacketFlow` method signatures are similar to `NEPacketTunnelFlow` so
// you can just extend that class to adopt `OpenVPNAdapterPacketFlow` protocol and
// send `self.packetFlow` to `completionHandler` callback.
func openVPNAdapter(
_ openVPNAdapter: OpenVPNAdapter,
configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?,
completionHandler: @escaping (Error?) -> Void
) {
// In order to direct all DNS queries first to the VPN DNS servers before the primary DNS servers
// send empty string to NEDNSSettings.matchDomains
networkSettings?.dnsSettings?.matchDomains = [""]
if splitTunnelType == 1 {
var ipv4IncludedRoutes = [NEIPv4Route]()
for allowedIPString in splitTunnelSites {
if let allowedIP = IPAddressRange(from: allowedIPString) {
ipv4IncludedRoutes.append(NEIPv4Route(
destinationAddress: "\(allowedIP.address)",
subnetMask: "\(allowedIP.subnetMask())"))
}
}
networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
} else {
if splitTunnelType == 2 {
var ipv4ExcludedRoutes = [NEIPv4Route]()
var ipv4IncludedRoutes = [NEIPv4Route]()
var ipv6IncludedRoutes = [NEIPv6Route]()
for excludeIPString in splitTunnelSites {
if let excludeIP = IPAddressRange(from: excludeIPString) {
ipv4ExcludedRoutes.append(NEIPv4Route(
destinationAddress: "\(excludeIP.address)",
subnetMask: "\(excludeIP.subnetMask())"))
}
}
if let allIPv4 = IPAddressRange(from: "0.0.0.0/0") {
ipv4IncludedRoutes.append(NEIPv4Route(
destinationAddress: "\(allIPv4.address)",
subnetMask: "\(allIPv4.subnetMask())"))
}
if let allIPv6 = IPAddressRange(from: "::/0") {
ipv6IncludedRoutes.append(NEIPv6Route(
destinationAddress: "\(allIPv6.address)",
networkPrefixLength: NSNumber(value: allIPv6.networkPrefixLength)))
}
networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
networkSettings?.ipv6Settings?.includedRoutes = ipv6IncludedRoutes
networkSettings?.ipv4Settings?.excludedRoutes = ipv4ExcludedRoutes
}
}
// Set the network settings for the current tunneling session.
setTunnelNetworkSettings(networkSettings, completionHandler: completionHandler)
}
// Process events returned by the OpenVPN library
func openVPNAdapter(
_ openVPNAdapter: OpenVPNAdapter,
handleEvent event: OpenVPNAdapterEvent,
message: String?) {
switch event {
case .connected:
if reasserting {
reasserting = false
}
guard let startHandler = startHandler else { return }
startHandler(nil)
self.startHandler = nil
case .disconnected:
guard let stopHandler = stopHandler else { return }
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
stopHandler()
self.stopHandler = nil
case .reconnecting:
reasserting = true
default:
break
}
}
// Handle errors thrown by the OpenVPN library
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleError error: Error) {
// Handle only fatal errors
guard let fatal = (error as NSError).userInfo[OpenVPNAdapterErrorFatalKey] as? Bool,
fatal == true else { return }
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
if let startHandler {
startHandler(error)
self.startHandler = nil
} else {
cancelTunnelWithError(error)
}
}
// Use this method to process any log message returned by OpenVPN library.
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleLogMessage logMessage: String) {
// Handle log messages
ovpnLog(.info, message: logMessage)
}
}
extension NEPacketTunnelFlow: OpenVPNAdapterPacketFlow {}

View File

@@ -1,129 +0,0 @@
import Foundation
import NetworkExtension
import OpenVPNAdapter
extension PacketTunnelProvider: OpenVPNAdapterDelegate {
// OpenVPNAdapter calls this delegate method to configure a VPN tunnel.
// `completionHandler` callback requires an object conforming to `OpenVPNAdapterPacketFlow`
// protocol if the tunnel is configured without errors. Otherwise send nil.
// `OpenVPNAdapterPacketFlow` method signatures are similar to `NEPacketTunnelFlow` so
// you can just extend that class to adopt `OpenVPNAdapterPacketFlow` protocol and
// send `self.packetFlow` to `completionHandler` callback.
func openVPNAdapter(
_ openVPNAdapter: OpenVPNAdapter,
configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?,
completionHandler: @escaping (Error?) -> Void
) {
// In order to direct all DNS queries first to the VPN DNS servers before the primary DNS servers
// send empty string to NEDNSSettings.matchDomains
networkSettings?.dnsSettings?.matchDomains = [""]
if splitTunnelType == "1" {
var ipv4IncludedRoutes = [NEIPv4Route]()
let STSdata = Data(splitTunnelSites!.utf8)
do {
guard let STSArray = try JSONSerialization.jsonObject(with: STSdata) as? [String] else { return }
for allowedIPString in STSArray {
if let allowedIP = IPAddressRange(from: allowedIPString) {
ipv4IncludedRoutes.append(NEIPv4Route(
destinationAddress: "\(allowedIP.address)",
subnetMask: "\(allowedIP.subnetMask())"))
}
}
} catch {
wg_log(.error, message: "Parse JSONSerialization Error")
}
networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
} else {
if splitTunnelType == "2" {
var ipv4ExcludedRoutes = [NEIPv4Route]()
var ipv4IncludedRoutes = [NEIPv4Route]()
var ipv6IncludedRoutes = [NEIPv6Route]()
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) {
ipv4ExcludedRoutes.append(NEIPv4Route(
destinationAddress: "\(excludeIP.address)",
subnetMask: "\(excludeIP.subnetMask())"))
}
}
} catch {
wg_log(.error, message: "Parse JSONSerialization Error")
}
if let allIPv4 = IPAddressRange(from: "0.0.0.0/0") {
ipv4IncludedRoutes.append(NEIPv4Route(
destinationAddress: "\(allIPv4.address)",
subnetMask: "\(allIPv4.subnetMask())"))
}
if let allIPv6 = IPAddressRange(from: "::/0") {
ipv6IncludedRoutes.append(NEIPv6Route(
destinationAddress: "\(allIPv6.address)",
networkPrefixLength: NSNumber(value: allIPv6.networkPrefixLength)))
}
networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
networkSettings?.ipv6Settings?.includedRoutes = ipv6IncludedRoutes
networkSettings?.ipv4Settings?.excludedRoutes = ipv4ExcludedRoutes
}
}
// Set the network settings for the current tunneling session.
setTunnelNetworkSettings(networkSettings, completionHandler: completionHandler)
}
// Process events returned by the OpenVPN library
func openVPNAdapter(
_ openVPNAdapter: OpenVPNAdapter,
handleEvent event: OpenVPNAdapterEvent,
message: String?) {
switch event {
case .connected:
if reasserting {
reasserting = false
}
guard let startHandler = startHandler else { return }
startHandler(nil)
self.startHandler = nil
case .disconnected:
guard let stopHandler = stopHandler else { return }
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
stopHandler()
self.stopHandler = nil
case .reconnecting:
reasserting = true
default:
break
}
}
// Handle errors thrown by the OpenVPN library
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleError error: Error) {
// Handle only fatal errors
guard let fatal = (error as NSError).userInfo[OpenVPNAdapterErrorFatalKey] as? Bool,
fatal == true else { return }
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
if let startHandler {
startHandler(error)
self.startHandler = nil
} else {
cancelTunnelWithError(error)
}
}
// Use this method to process any log message returned by OpenVPN library.
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleLogMessage logMessage: String) {
// Handle log messages
wg_log(.info, message: logMessage)
}
}

View File

@@ -0,0 +1,221 @@
import Foundation
import NetworkExtension
extension PacketTunnelProvider {
func startWireguard(activationAttemptId: String?,
errorNotifier: ErrorNotifier,
completionHandler: @escaping (Error?) -> Void) {
guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol,
let providerConfiguration = protocolConfiguration.providerConfiguration,
let wgConfigData: Data = providerConfiguration[Constants.wireGuardConfigKey] as? Data else {
wg_log(.error, message: "Can't start, config missing")
completionHandler(nil)
return
}
do {
let wgConfig = try JSONDecoder().decode(WGConfig.self, from: wgConfigData)
let wgConfigStr = wgConfig.str
wg_log(.info, title: "config: ", message: wgConfig.redux)
let tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: wgConfigStr)
if tunnelConfiguration.peers.first!.allowedIPs
.map({ $0.stringRepresentation })
.joined(separator: ", ") == "0.0.0.0/0, ::/0" {
if wgConfig.splitTunnelType == 1 {
for index in tunnelConfiguration.peers.indices {
tunnelConfiguration.peers[index].allowedIPs.removeAll()
var allowedIPs = [IPAddressRange]()
for allowedIPString in wgConfig.splitTunnelSites {
if let allowedIP = IPAddressRange(from: allowedIPString) {
allowedIPs.append(allowedIP)
}
}
tunnelConfiguration.peers[index].allowedIPs = allowedIPs
}
} else if wgConfig.splitTunnelType == 2 {
for index in tunnelConfiguration.peers.indices {
var excludeIPs = [IPAddressRange]()
for excludeIPString in wgConfig.splitTunnelSites {
if let excludeIP = IPAddressRange(from: excludeIPString) {
excludeIPs.append(excludeIP)
}
}
tunnelConfiguration.peers[index].excludeIPs = excludeIPs
}
}
}
wg_log(.info, message: "Starting tunnel from the " +
(activationAttemptId == nil ? "OS directly, rather than the app" : "app"))
// Start the tunnel
wgAdapter.start(tunnelConfiguration: tunnelConfiguration) { adapterError in
guard let adapterError else {
let interfaceName = self.wgAdapter.interfaceName ?? "unknown"
wg_log(.info, message: "Tunnel interface is \(interfaceName)")
completionHandler(nil)
return
}
switch adapterError {
case .cannotLocateTunnelFileDescriptor:
wg_log(.error, staticMessage: "Starting tunnel failed: could not determine file descriptor")
errorNotifier.notify(PacketTunnelProviderError.couldNotDetermineFileDescriptor)
completionHandler(PacketTunnelProviderError.couldNotDetermineFileDescriptor)
case .dnsResolution(let dnsErrors):
let hostnamesWithDnsResolutionFailure = dnsErrors.map { $0.address }
.joined(separator: ", ")
wg_log(.error, message:
"DNS resolution failed for the following hostnames: \(hostnamesWithDnsResolutionFailure)")
errorNotifier.notify(PacketTunnelProviderError.dnsResolutionFailure)
completionHandler(PacketTunnelProviderError.dnsResolutionFailure)
case .setNetworkSettings(let error):
wg_log(.error, message:
"Starting tunnel failed with setTunnelNetworkSettings returning \(error.localizedDescription)")
errorNotifier.notify(PacketTunnelProviderError.couldNotSetNetworkSettings)
completionHandler(PacketTunnelProviderError.couldNotSetNetworkSettings)
case .startWireGuardBackend(let errorCode):
wg_log(.error, message: "Starting tunnel failed with wgTurnOn returning \(errorCode)")
errorNotifier.notify(PacketTunnelProviderError.couldNotStartBackend)
completionHandler(PacketTunnelProviderError.couldNotStartBackend)
case .invalidState:
fatalError()
}
}
} catch {
wg_log(.error, message: "Can't parse WG config: \(error.localizedDescription)")
completionHandler(nil)
return
}
}
func handleWireguardStatusMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
guard let completionHandler = completionHandler else { return }
wgAdapter.getRuntimeConfiguration { settings in
var data: Data?
if let settings {
data = settings.data(using: .utf8)!
}
let components = settings!.components(separatedBy: "\n")
var settingsDictionary: [String: String] = [:]
for component in components {
let pair = component.components(separatedBy: "=")
if pair.count == 2 {
settingsDictionary[pair[0]] = pair[1]
}
}
let response: [String: Any] = [
"rx_bytes": settingsDictionary["rx_bytes"] ?? "0",
"tx_bytes": settingsDictionary["tx_bytes"] ?? "0"
]
completionHandler(try? JSONSerialization.data(withJSONObject: response, options: []))
}
}
private func handleWireguardAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
guard let completionHandler = completionHandler else { return }
if messageData.count == 1 && messageData[0] == 0 {
wgAdapter.getRuntimeConfiguration { settings in
var data: Data?
if let settings {
data = settings.data(using: .utf8)!
}
completionHandler(data)
}
} else if messageData.count >= 1 {
// Updates the tunnel configuration and responds with the active configuration
wg_log(.info, message: "Switching tunnel configuration")
guard let configString = String(data: messageData, encoding: .utf8)
else {
completionHandler(nil)
return
}
do {
let tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: configString)
wgAdapter.update(tunnelConfiguration: tunnelConfiguration) { error in
if let error {
wg_log(.error, message: "Failed to switch tunnel configuration: \(error.localizedDescription)")
completionHandler(nil)
return
}
self.wgAdapter.getRuntimeConfiguration { settings in
var data: Data?
if let settings {
data = settings.data(using: .utf8)!
}
completionHandler(data)
}
}
} catch {
completionHandler(nil)
}
} else {
completionHandler(nil)
}
}
// private func startEmptyTunnel(completionHandler: @escaping (Error?) -> Void) {
// dispatchPrecondition(condition: .onQueue(dispatchQueue))
//
// let emptyTunnelConfiguration = TunnelConfiguration(
// name: nil,
// interface: InterfaceConfiguration(privateKey: PrivateKey()),
// peers: []
// )
//
// wgAdapter.start(tunnelConfiguration: emptyTunnelConfiguration) { error in
// self.dispatchQueue.async {
// if let error {
// wg_log(.error, message: "Failed to start an empty tunnel")
// completionHandler(error)
// } else {
// wg_log(.info, message: "Started an empty tunnel")
// self.tunnelAdapterDidStart()
// }
// }
// }
//
// let settings = NETunnelNetworkSettings(tunnelRemoteAddress: "1.1.1.1")
//
// self.setTunnelNetworkSettings(settings) { error in
// completionHandler(error)
// }
// }
// private func tunnelAdapterDidStart() {
// dispatchPrecondition(condition: .onQueue(dispatchQueue))
// // ...
// }
func stopWireguard(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
wg_log(.info, staticMessage: "Stopping tunnel")
wgAdapter.stop { error in
ErrorNotifier.removeLastErrorFile()
if let error {
wg_log(.error, message: "Failed to stop WireGuard adapter: \(error.localizedDescription)")
}
completionHandler()
#if os(macOS)
// HACK: This is a filthy hack to work around Apple bug 32073323 (dup'd by us as 47526107).
// Remove it when they finally fix this upstream and the fix has been rolled out to
// sufficient quantities of users.
exit(0)
#endif
}
}
}

View File

@@ -15,7 +15,7 @@ struct Constants {
static let ovpnConfigKey = "ovpn"
static let wireGuardConfigKey = "wireguard"
static let loggerTag = "NET"
static let kActionStart = "start"
static let kActionRestart = "restart"
static let kActionStop = "stop"
@@ -34,82 +34,68 @@ struct Constants {
}
class PacketTunnelProvider: NEPacketTunnelProvider {
private lazy var wgAdapter = {
lazy var wgAdapter = {
WireGuardAdapter(with: self) { logLevel, message in
wg_log(logLevel.osLogLevel, message: message)
}
}()
private lazy var ovpnAdapter: OpenVPNAdapter = {
lazy var ovpnAdapter: OpenVPNAdapter = {
let adapter = OpenVPNAdapter()
adapter.delegate = self
return adapter
}()
/// Internal queue.
private let dispatchQueue = DispatchQueue(label: "PacketTunnel", qos: .utility)
private var openVPNConfig: Data?
var splitTunnelType: String?
var splitTunnelSites: String?
var splitTunnelType: Int!
var splitTunnelSites: [String]!
let vpnReachability = OpenVPNReachability()
var startHandler: ((Error?) -> Void)?
var stopHandler: (() -> Void)?
var protoType: TunnelProtoType = .none
override init() {
super.init()
}
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
let tmpStr = String(data: messageData, encoding: .utf8)!
wg_log(.error, message: tmpStr)
guard let message = String(data: messageData, encoding: .utf8) else {
if let completionHandler {
completionHandler(nil)
}
return
}
neLog(.info, title: "App said: ", message: message)
guard let message = try? JSONSerialization.jsonObject(with: messageData, options: []) as? [String: Any] else {
log(.error, message: "Failed to serialize message from app")
neLog(.error, message: "Failed to serialize message from app")
return
}
guard let completionHandler = completionHandler else {
log(.error, message: "Missing message completion handler")
guard let completionHandler else {
neLog(.error, message: "Missing message completion handler")
return
}
guard let action = message[Constants.kMessageKeyAction] as? String else {
log(.error, message: "Missing action key in app message")
neLog(.error, message: "Missing action key in app message")
completionHandler(nil)
return
}
if action == Constants.kActionStatus {
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) {
dispatchQueue.async {
let activationAttemptId = options?[Constants.kActivationAttemptId] as? String
let errorNotifier = ErrorNotifier(activationAttemptId: activationAttemptId)
log(.info, message: "PacketTunnelProvider startTunnel")
neLog(.info, message: "Start tunnel")
if let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol {
let providerConfiguration = protocolConfiguration.providerConfiguration
if (providerConfiguration?[Constants.ovpnConfigKey] as? Data) != nil {
@@ -120,7 +106,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
} else {
self.protoType = .none
}
switch self.protoType {
case .wireguard:
self.startWireguard(activationAttemptId: activationAttemptId,
@@ -136,7 +122,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
}
}
}
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
dispatchQueue.async {
switch self.protoType {
@@ -152,7 +138,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
}
}
}
func handleStatusAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
switch protoType {
case .wireguard:
@@ -166,281 +152,18 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
break
}
}
// MARK: Private methods
private func startWireguard(activationAttemptId: String?,
errorNotifier: ErrorNotifier,
completionHandler: @escaping (Error?) -> Void) {
guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol,
let providerConfiguration = protocolConfiguration.providerConfiguration,
let wgConfig: Data = providerConfiguration[Constants.wireGuardConfigKey] as? Data else {
wg_log(.error, message: "Can't start WireGuard config missing")
completionHandler(nil)
return
}
let wgConfigStr = String(data: wgConfig, encoding: .utf8)!
guard let tunnelConfiguration = try? TunnelConfiguration(fromWgQuickConfig: wgConfigStr) else {
wg_log(.error, message: "Can't parse WireGuard config")
completionHandler(nil)
return
}
if tunnelConfiguration.peers.first!.allowedIPs
.map({ $0.stringRepresentation })
.joined(separator: ", ") == "0.0.0.0/0, ::/0" {
if splitTunnelType == "1" {
for index in tunnelConfiguration.peers.indices {
tunnelConfiguration.peers[index].allowedIPs.removeAll()
var allowedIPs = [IPAddressRange]()
let STSdata = Data(splitTunnelSites!.utf8)
do {
guard let STSArray = try JSONSerialization.jsonObject(with: STSdata) as? [String] else { return }
for allowedIPString in STSArray {
if let allowedIP = IPAddressRange(from: allowedIPString) {
allowedIPs.append(allowedIP)
}
}
} catch {
wg_log(.error, message: "Parse JSONSerialization Error")
}
tunnelConfiguration.peers[index].allowedIPs = allowedIPs
}
} else if splitTunnelType == "2" {
for index in tunnelConfiguration.peers.indices {
var excludeIPs = [IPAddressRange]()
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) {
excludeIPs.append(excludeIP)
}
}
} catch {
wg_log(.error, message: "Parse JSONSerialization Error")
}
tunnelConfiguration.peers[index].excludeIPs = excludeIPs
}
}
}
wg_log(.info, message: "Starting wireguard tunnel from the " +
(activationAttemptId == nil ? "OS directly, rather than the app" : "app"))
// Start the tunnel
wgAdapter.start(tunnelConfiguration: tunnelConfiguration) { adapterError in
guard let adapterError else {
let interfaceName = self.wgAdapter.interfaceName ?? "unknown"
wg_log(.info, message: "Tunnel interface is \(interfaceName)")
completionHandler(nil)
return
}
switch adapterError {
case .cannotLocateTunnelFileDescriptor:
wg_log(.error, staticMessage: "Starting tunnel failed: could not determine file descriptor")
errorNotifier.notify(PacketTunnelProviderError.couldNotDetermineFileDescriptor)
completionHandler(PacketTunnelProviderError.couldNotDetermineFileDescriptor)
case .dnsResolution(let dnsErrors):
let hostnamesWithDnsResolutionFailure = dnsErrors.map { $0.address }
.joined(separator: ", ")
wg_log(.error, message:
"DNS resolution failed for the following hostnames: \(hostnamesWithDnsResolutionFailure)")
errorNotifier.notify(PacketTunnelProviderError.dnsResolutionFailure)
completionHandler(PacketTunnelProviderError.dnsResolutionFailure)
case .setNetworkSettings(let error):
wg_log(.error, message:
"Starting tunnel failed with setTunnelNetworkSettings returning \(error.localizedDescription)")
errorNotifier.notify(PacketTunnelProviderError.couldNotSetNetworkSettings)
completionHandler(PacketTunnelProviderError.couldNotSetNetworkSettings)
case .startWireGuardBackend(let errorCode):
wg_log(.error, message: "Starting tunnel failed with wgTurnOn returning \(errorCode)")
errorNotifier.notify(PacketTunnelProviderError.couldNotStartBackend)
completionHandler(PacketTunnelProviderError.couldNotStartBackend)
case .invalidState:
fatalError()
}
}
}
private func startOpenVPN(completionHandler: @escaping (Error?) -> Void) {
guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol,
let providerConfiguration = protocolConfiguration.providerConfiguration,
let ovpnConfiguration: Data = providerConfiguration[Constants.ovpnConfigKey] as? Data else {
wg_log(.error, message: "Can't start startOpenVPN()")
return
}
setupAndlaunchOpenVPN(withConfig: ovpnConfiguration, completionHandler: completionHandler)
}
private func stopWireguard(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
wg_log(.info, staticMessage: "Stopping tunnel")
wgAdapter.stop { error in
ErrorNotifier.removeLastErrorFile()
if let error {
wg_log(.error, message: "Failed to stop WireGuard adapter: \(error.localizedDescription)")
}
completionHandler()
#if os(macOS)
// HACK: This is a filthy hack to work around Apple bug 32073323 (dup'd by us as 47526107).
// Remove it when they finally fix this upstream and the fix has been rolled out to
// sufficient quantities of users.
exit(0)
#endif
}
}
private func stopOpenVPN(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
stopHandler = completionHandler
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
ovpnAdapter.disconnect()
}
func handleWireguardStatusMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
guard let completionHandler = completionHandler else { return }
wgAdapter.getRuntimeConfiguration { settings in
var data: Data?
if let settings {
data = settings.data(using: .utf8)!
}
let components = settings!.components(separatedBy: "\n")
var settingsDictionary: [String: String] = [:]
for component in components {
let pair = component.components(separatedBy: "=")
if pair.count == 2 {
settingsDictionary[pair[0]] = pair[1]
}
}
let response: [String: Any] = [
"rx_bytes": settingsDictionary["rx_bytes"] ?? "0",
"tx_bytes": settingsDictionary["tx_bytes"] ?? "0"
]
completionHandler(try? JSONSerialization.data(withJSONObject: response, options: []))
}
}
private func handleWireguardAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
guard let completionHandler = completionHandler else { return }
if messageData.count == 1 && messageData[0] == 0 {
wgAdapter.getRuntimeConfiguration { settings in
var data: Data?
if let settings {
data = settings.data(using: .utf8)!
}
completionHandler(data)
}
} else if messageData.count >= 1 {
// Updates the tunnel configuration and responds with the active configuration
wg_log(.info, message: "Switching tunnel configuration")
guard let configString = String(data: messageData, encoding: .utf8)
else {
completionHandler(nil)
return
}
do {
let tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: configString)
wgAdapter.update(tunnelConfiguration: tunnelConfiguration) { error in
if let error {
wg_log(.error, message: "Failed to switch tunnel configuration: \(error.localizedDescription)")
completionHandler(nil)
return
}
self.wgAdapter.getRuntimeConfiguration { settings in
var data: Data?
if let settings {
data = settings.data(using: .utf8)!
}
completionHandler(data)
}
}
} catch {
completionHandler(nil)
}
} else {
completionHandler(nil)
}
}
private func handleOpenVPNStatusMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
guard let completionHandler = completionHandler else { return }
let bytesin = ovpnAdapter.transportStatistics.bytesIn
let bytesout = ovpnAdapter.transportStatistics.bytesOut
let response: [String: Any] = [
"rx_bytes": bytesin,
"tx_bytes": bytesout
]
completionHandler(try? JSONSerialization.data(withJSONObject: response, options: []))
}
private func setupAndlaunchOpenVPN(withConfig ovpnConfiguration: Data,
withShadowSocks viaSS: Bool = false,
completionHandler: @escaping (Error?) -> Void) {
wg_log(.info, message: "setupAndlaunchOpenVPN")
let str = String(decoding: ovpnConfiguration, as: UTF8.self)
let configuration = OpenVPNConfiguration()
configuration.fileContent = ovpnConfiguration
if str.contains("cloak") {
configuration.setPTCloak()
}
let evaluation: OpenVPNConfigurationEvaluation
do {
evaluation = try ovpnAdapter.apply(configuration: configuration)
} catch {
completionHandler(error)
return
}
if !evaluation.autologin {
wg_log(.info, message: "Implement login with user credentials")
}
vpnReachability.startTracking { [weak self] status in
guard status == .reachableViaWiFi else { return }
self?.ovpnAdapter.reconnect(afterTimeInterval: 5)
}
startHandler = completionHandler
ovpnAdapter.connect(using: packetFlow)
// let ifaces = Interface.allInterfaces()
// .filter { $0.family == .ipv4 }
// .map { iface in iface.name }
// wg_log(.error, message: "Available TUN Interfaces: \(ifaces)")
}
// MARK: Network observing methods
private func startListeningForNetworkChanges() {
stopListeningForNetworkChanges()
addObserver(self, forKeyPath: Constants.kDefaultPathKey, options: .old, context: nil)
}
private func stopListeningForNetworkChanges() {
removeObserver(self, forKeyPath: Constants.kDefaultPathKey)
}
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey: Any]?,
@@ -460,48 +183,13 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
self.handle(networkChange: self.defaultPath!) { _ in }
}
}
private func handle(networkChange changePath: NWPath, completion: @escaping (Error?) -> Void) {
wg_log(.info, message: "Tunnel restarted.")
startTunnel(options: nil, completionHandler: completion)
}
private func startEmptyTunnel(completionHandler: @escaping (Error?) -> Void) {
dispatchPrecondition(condition: .onQueue(dispatchQueue))
let emptyTunnelConfiguration = TunnelConfiguration(
name: nil,
interface: InterfaceConfiguration(privateKey: PrivateKey()),
peers: []
)
wgAdapter.start(tunnelConfiguration: emptyTunnelConfiguration) { error in
self.dispatchQueue.async {
if let error {
log(.error, message: "Failed to start an empty tunnel")
completionHandler(error)
} else {
log(.info, message: "Started an empty tunnel")
self.tunnelAdapterDidStart()
}
}
}
let settings = NETunnelNetworkSettings(tunnelRemoteAddress: "1.1.1.1")
self.setTunnelNetworkSettings(settings) { error in
completionHandler(error)
}
}
private func tunnelAdapterDidStart() {
dispatchPrecondition(condition: .onQueue(dispatchQueue))
// ...
}
}
extension NEPacketTunnelFlow: OpenVPNAdapterPacketFlow {}
extension WireGuardLogLevel {
var osLogLevel: OSLogType {
switch self {

View File

@@ -1,4 +1,7 @@
#import <UIKit/UIKit.h>
@interface QtAppDelegate : UIResponder <UIApplicationDelegate>
@interface QIOSApplicationDelegate
@end
@interface QIOSApplicationDelegate (AmneziaVPNDelegate)
@end

View File

@@ -3,41 +3,17 @@
#include <QFile>
@implementation QtAppDelegate {
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
{
[application setMinimumBackgroundFetchInterval: UIApplicationBackgroundFetchIntervalMinimum];
// Override point for customization after application launch.
NSLog(@"Did this launch option happen");
NSLog(@"Application didFinishLaunchingWithOptions");
return YES;
}
- (void)applicationWillResignActive:(UIApplication *)application
{
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
_screen = [UIScreen.mainScreen snapshotViewAfterScreenUpdates: false];
UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle: UIBlurEffectStyleDark];
UIVisualEffectView *blurBackground = [[UIVisualEffectView alloc] initWithEffect: blurEffect];
[_screen addSubview: blurBackground];
blurBackground.frame = _screen.frame;
UIWindow *_window = UIApplication.sharedApplication.keyWindow;
[_window addSubview: _screen];
}
- (void)applicationDidEnterBackground:(UIApplication *)application
{
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
@@ -51,17 +27,6 @@
NSLog(@"In the foreground");
}
- (void)applicationDidBecomeActive:(UIApplication *)application
{
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
[_screen removeFromSuperview];
}
- (void)applicationWillTerminate:(UIApplication *)application
{
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
-(void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
// We will add content here soon.
NSLog(@"In the completionHandler");
@@ -70,31 +35,27 @@
- (BOOL)application:(UIApplication *)app
openURL:(NSURL *)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
NSLog(@"Application openURL: %@", url);
if (url.fileURL) {
QString filePath(url.path.UTF8String);
if (filePath.isEmpty()) return NO;
if (filePath.contains("backup")) {
IosController::Instance()->importBackupFromOutside(filePath);
} else {
QFile file(filePath);
bool isOpenFile = file.open(QIODevice::ReadOnly);
QByteArray data = file.readAll();
IosController::Instance()->importConfigFromOutside(QString(data));
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
NSLog(@"Application openURL: %@", url);
if (filePath.contains("backup")) {
IosController::Instance()->importBackupFromOutside(filePath);
} else {
QFile file(filePath);
bool isOpenFile = file.open(QIODevice::ReadOnly);
QByteArray data = file.readAll();
IosController::Instance()->importConfigFromOutside(QString(data));
}
});
return YES;
}
return NO;
}
void QtAppDelegateInitialize()
{
[[UIApplication sharedApplication] setDelegate: [QtAppDelegate sharedQtAppDelegate]];
NSLog(@"Created a new AppDelegate");
}
@end

View File

@@ -0,0 +1,87 @@
import UIKit
public func toggleScreenshots(_ isEnabled: Bool) {
let window = UIApplication.shared.keyWindows.first!
if isEnabled {
ScreenProtection.shared.disable(for: window.rootViewController!.view)
} else {
ScreenProtection.shared.enable(for: window.rootViewController!.view)
}
}
extension UIApplication {
var keyWindows: [UIWindow] {
connectedScenes
.compactMap {
if #available(iOS 15.0, *) {
($0 as? UIWindowScene)?.keyWindow
} else {
($0 as? UIWindowScene)?.windows.first { $0.isKeyWindow }
}
}
}
}
class ScreenProtection {
public static let shared = ScreenProtection()
var pairs = [ProtectionPair]()
private var blurView: UIVisualEffectView?
private var recordingObservation: NSKeyValueObservation?
public func enable(for view: UIView) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
view.subviews.forEach {
self.pairs.append(ProtectionPair(from: $0))
}
}
}
public func disable(for view: UIView) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.pairs.forEach {
$0.removeProtection()
}
self.pairs.removeAll()
}
}
}
struct ProtectionPair {
let textField: UITextField
let layer: CALayer
init(from view: UIView) {
let secureTextField = UITextField()
secureTextField.backgroundColor = .clear
secureTextField.translatesAutoresizingMaskIntoConstraints = false
secureTextField.isSecureTextEntry = true
view.insertSubview(secureTextField, at: 0)
secureTextField.isUserInteractionEnabled = false
view.layer.superlayer?.addSublayer(secureTextField.layer)
secureTextField.layer.sublayers?.last?.addSublayer(view.layer)
secureTextField.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
secureTextField.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
secureTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true
secureTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0).isActive = true
self.init(textField: secureTextField, layer: view.layer)
}
init(textField: UITextField, layer: CALayer) {
self.textField = textField
self.layer = layer
}
func removeProtection() {
textField.superview?.superview?.layer.addSublayer(layer)
textField.layer.removeFromSuperlayer()
textField.removeFromSuperview()
}
}

View File

@@ -0,0 +1,91 @@
import Foundation
struct WGConfig: Decodable {
let initPacketMagicHeader, responsePacketMagicHeader: String?
let underloadPacketMagicHeader, transportPacketMagicHeader: String?
let junkPacketCount, junkPacketMinSize, junkPacketMaxSize: String?
let initPacketJunkSize, responsePacketJunkSize: String?
let dns1: String
let dns2: String
let mtu: 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 mtu
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 {
junkPacketCount == nil ? "" :
"""
Jc = \(junkPacketCount!)
Jmin = \(junkPacketMinSize!)
Jmax = \(junkPacketMaxSize!)
S1 = \(initPacketJunkSize!)
S2 = \(responsePacketJunkSize!)
H1 = \(initPacketMagicHeader!)
H2 = \(responsePacketMagicHeader!)
H3 = \(underloadPacketMagicHeader!)
H4 = \(transportPacketMagicHeader!)
"""
}
var str: String {
"""
[Interface]
Address = \(clientIP)
DNS = \(dns1), \(dns2)
MTU = \(mtu)
PrivateKey = \(clientPrivateKey)
\(settings)
[Peer]
PublicKey = \(serverPublicKey)
PresharedKey = \(presharedKey)
AllowedIPs = \(allowedIPs.joined(separator: ", "))
Endpoint = \(hostName):\(port)
PersistentKeepalive = \(persistentKeepAlive)
"""
}
var redux: String {
"""
[Interface]
Address = \(clientIP)
DNS = \(dns1), \(dns2)
MTU = \(mtu)
PrivateKey = ***
\(settings)
[Peer]
PublicKey = ***
PresharedKey = ***
AllowedIPs = \(allowedIPs.joined(separator: ", "))
Endpoint = \(hostName):\(port)
PersistentKeepalive = \(persistentKeepAlive)
"""
}
}

View File

@@ -235,7 +235,6 @@ void IosController::checkStatus()
m_rxBytes = rxBytes;
m_txBytes = txBytes;
});
}
void IosController::vpnStatusDidChange(void *pNotification)
@@ -244,13 +243,13 @@ void IosController::vpnStatusDidChange(void *pNotification)
if (session /* && session == TunnelManager.session */ ) {
qDebug() << "IosController::vpnStatusDidChange" << iosStatusToState(session.status) << session;
if (session.status == NEVPNStatusDisconnected) {
if (@available(iOS 16.0, *)) {
[session fetchLastDisconnectErrorWithCompletionHandler:^(NSError * _Nullable error) {
if (error != nil) {
qDebug() << "Disconnect error" << error.domain << error.code << error.localizedDescription;
if ([error.domain isEqualToString:NEVPNConnectionErrorDomain]) {
switch (error.code) {
case NEVPNConnectionErrorOverslept:
@@ -315,11 +314,11 @@ void IosController::vpnStatusDidChange(void *pNotification)
break;
}
}
NSError *underlyingError = error.userInfo[@"NSUnderlyingError"];
if (underlyingError != nil) {
qDebug() << "Disconnect underlying error" << underlyingError.domain << underlyingError.code << underlyingError.localizedDescription;
if ([underlyingError.domain isEqualToString:@"NEAgentErrorDomain"]) {
switch (underlyingError.code) {
case 1:
@@ -342,7 +341,7 @@ void IosController::vpnStatusDidChange(void *pNotification)
qDebug() << "Disconnect error is unavailable on iOS < 16.0";
}
}
emit connectionStateChanged(iosStatusToState(session.status));
}
}
@@ -357,7 +356,29 @@ bool IosController::setupOpenVPN()
QJsonObject ovpn = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::OpenVpn)].toObject();
QString ovpnConfig = ovpn[config_key::config].toString();
return startOpenVPN(ovpnConfig);
QJsonObject openVPNConfig {};
openVPNConfig.insert(config_key::config, ovpnConfig);
if (ovpn.contains(config_key::mtu)) {
openVPNConfig.insert(config_key::mtu, ovpn[config_key::mtu]);
} else {
openVPNConfig.insert(config_key::mtu, protocols::openvpn::defaultMtu);
}
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()
@@ -394,25 +415,137 @@ bool IosController::setupCloak()
ovpnConfig.append(cloakBase64);
ovpnConfig.append("\n</cloak>\n");
return startOpenVPN(ovpnConfig);
QJsonObject openVPNConfig {};
openVPNConfig.insert(config_key::config, ovpnConfig);
if (ovpn.contains(config_key::mtu)) {
openVPNConfig.insert(config_key::mtu, ovpn[config_key::mtu]);
} else {
openVPNConfig.insert(config_key::mtu, protocols::openvpn::defaultMtu);
}
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()
{
QJsonObject config = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::WireGuard)].toObject();
QString wgConfig = config[config_key::config].toString();
return startWireGuard(wgConfig);
QJsonObject wgConfig {};
wgConfig.insert(config_key::dns1, m_rawConfig[config_key::dns1]);
wgConfig.insert(config_key::dns2, m_rawConfig[config_key::dns2]);
if (config.contains(config_key::mtu)) {
wgConfig.insert(config_key::mtu, config[config_key::mtu]);
} else {
wgConfig.insert(config_key::mtu, protocols::wireguard::defaultMtu);
}
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]);
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) && config[config_key::allowed_ips].isArray()) {
wgConfig.insert(config_key::allowed_ips, config[config_key::allowed_ips]);
} else {
QJsonArray allowed_ips { "0.0.0.0/0", "::/0" };
wgConfig.insert(config_key::allowed_ips, allowed_ips);
}
if (config.contains(config_key::persistent_keep_alive)) {
wgConfig.insert(config_key::persistent_keep_alive, config[config_key::persistent_keep_alive]);
} else {
wgConfig.insert(config_key::persistent_keep_alive, "25");
}
QJsonDocument wgConfigDoc(wgConfig);
QString wgConfigDocStr(wgConfigDoc.toJson(QJsonDocument::Compact));
return startWireGuard(wgConfigDocStr);
}
bool IosController::setupAwg()
{
QJsonObject config = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::Awg)].toObject();
QString wgConfig = config[config_key::config].toString();
return startWireGuard(wgConfig);
QJsonObject wgConfig {};
wgConfig.insert(config_key::dns1, m_rawConfig[config_key::dns1]);
wgConfig.insert(config_key::dns2, m_rawConfig[config_key::dns2]);
if (config.contains(config_key::mtu)) {
wgConfig.insert(config_key::mtu, config[config_key::mtu]);
} else {
wgConfig.insert(config_key::mtu, protocols::awg::defaultMtu);
}
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]);
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) && config[config_key::allowed_ips].isArray()) {
wgConfig.insert(config_key::allowed_ips, config[config_key::allowed_ips]);
} else {
QJsonArray allowed_ips { "0.0.0.0/0", "::/0" };
wgConfig.insert(config_key::allowed_ips, allowed_ips);
}
if (config.contains(config_key::persistent_keep_alive)) {
wgConfig.insert(config_key::persistent_keep_alive, config[config_key::persistent_keep_alive]);
} else {
wgConfig.insert(config_key::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)
@@ -446,23 +579,17 @@ bool IosController::startWireGuard(const QString &config)
void IosController::startTunnel()
{
NSString *protocolName = @"Unknown";
NETunnelProviderProtocol *tunnelProtocol = (NETunnelProviderProtocol *)m_currentTunnel.protocolConfiguration;
if (tunnelProtocol.providerConfiguration[@"wireguard"] != nil) {
protocolName = @"WireGuard";
} else if (tunnelProtocol.providerConfiguration[@"ovpn"] != nil) {
protocolName = @"OpenVPN";
}
m_rxBytes = 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 saveToPreferencesWithCompletionHandler:^(NSError *saveError) {
@@ -483,23 +610,6 @@ void IosController::startTunnel()
NSError *startError = nil;
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];
if (!started || startError) {
@@ -514,7 +624,6 @@ void IosController::startTunnel()
}];
}
bool IosController::isOurManager(NETunnelProviderManager* manager) {
NETunnelProviderProtocol* tunnelProto = (NETunnelProviderProtocol*)manager.protocolConfiguration;
@@ -576,7 +685,7 @@ void IosController::sendVpnExtensionMessage(NSDictionary* message, std::function
NETunnelProviderSession *session = (NETunnelProviderSession *)m_currentTunnel.connection;
NSError *sendError = nil;
if ([session respondsToSelector:@selector(sendProviderMessage:returnError:responseHandler:)]) {
[session sendProviderMessage:data returnError:&sendError responseHandler:completionHandler];
} else {

View File

@@ -21,7 +21,7 @@
}
- (void) vpnConfigurationDidChange:(NSNotification *)notification {
cppController->vpnStatusDidChange(notification);
// cppController->vpnStatusDidChange(notification);
}

View File

@@ -16,9 +16,6 @@
#include "leakdetector.h"
#include "logger.h"
constexpr uint32_t ETH_MTU = 1500;
constexpr uint32_t WG_MTU_OVERHEAD = 80;
namespace {
Logger logger("IPUtilsLinux");
}
@@ -38,8 +35,6 @@ bool IPUtilsLinux::addInterfaceIPs(const InterfaceConfig& config) {
}
bool IPUtilsLinux::setMTUAndUp(const InterfaceConfig& config) {
Q_UNUSED(config);
// Create socket file descriptor to perform the ioctl operations on
int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
if (sockfd < 0) {
@@ -56,10 +51,10 @@ bool IPUtilsLinux::setMTUAndUp(const InterfaceConfig& config) {
// FIXME: We need to know how many layers deep this particular
// interface is into a tunnel to work effectively. Otherwise
// we will run into fragmentation issues.
ifr.ifr_mtu = ETH_MTU - WG_MTU_OVERHEAD;
ifr.ifr_mtu = config.m_deviceMTU;
int ret = ioctl(sockfd, SIOCSIFMTU, &ifr);
if (ret) {
logger.error() << "Failed to set MTU -- Return code: " << ret;
logger.error() << "Failed to set MTU -- " << config.m_deviceMTU << " -- Return code: " << ret;
return false;
}

View File

@@ -20,9 +20,6 @@
#include "logger.h"
#include "macosdaemon.h"
constexpr uint32_t ETH_MTU = 1500;
constexpr uint32_t WG_MTU_OVERHEAD = 80;
namespace {
Logger logger("IPUtilsMacos");
}
@@ -56,10 +53,10 @@ bool IPUtilsMacos::setMTUAndUp(const InterfaceConfig& config) {
// MTU
strncpy(ifr.ifr_name, qPrintable(ifname), IFNAMSIZ);
ifr.ifr_mtu = ETH_MTU - WG_MTU_OVERHEAD;
ifr.ifr_mtu = config.m_deviceMTU;
int ret = ioctl(sockfd, SIOCSIFMTU, &ifr);
if (ret) {
logger.error() << "Failed to set MTU:" << strerror(errno);
logger.error() << "Failed to set MTU -- " << config.m_deviceMTU << " -- Return code: " << ret;
return false;
}

View File

@@ -58,7 +58,6 @@ void WindowsTunnelService::stop() {
if (m_logworker) {
m_logthread.quit();
m_logthread.wait();
delete m_logworker;
m_logworker = nullptr;
}
}
@@ -104,6 +103,7 @@ bool WindowsTunnelService::start(const QString& configData) {
m_logworker = new WindowsTunnelLogger(WindowsCommons::tunnelLogFile());
m_logworker->moveToThread(&m_logthread);
connect(&m_logthread, &QThread::finished, m_logworker, &QObject::deleteLater);
m_logthread.start();
SC_HANDLE scm = (SC_HANDLE)m_scm;

View File

@@ -217,7 +217,7 @@ ErrorCode OpenVpnProtocol::start()
return ErrorCode::AmneziaServiceConnectionFailed;
}
m_openVpnProcess->waitForSource(1000);
m_openVpnProcess->waitForSource(5000);
if (!m_openVpnProcess->isInitialized()) {
qWarning() << "IpcProcess replica is not connected!";
setLastError(ErrorCode::AmneziaServiceConnectionFailed);

View File

@@ -20,6 +20,7 @@ namespace amnezia
constexpr char dns1[] = "dns1";
constexpr char dns2[] = "dns2";
constexpr char serverIndex[] = "serverIndex";
constexpr char description[] = "description";
constexpr char name[] = "name";
constexpr char cert[] = "cert";
@@ -44,7 +45,9 @@ namespace amnezia
constexpr char server_priv_key[] = "server_priv_key";
constexpr char server_pub_key[] = "server_pub_key";
constexpr char psk_key[] = "psk_key";
constexpr char mtu[] = "mtu";
constexpr char allowed_ips[] = "allowed_ips";
constexpr char persistent_keep_alive[] = "persistent_keep_alive";
constexpr char client_ip[] = "client_ip"; // internal ip address
@@ -102,6 +105,7 @@ namespace amnezia
constexpr char defaultSubnetAddress[] = "10.8.0.0";
constexpr char defaultSubnetMask[] = "255.255.255.0";
constexpr char defaultSubnetCidr[] = "24";
constexpr char defaultMtu[] = "1500";
constexpr char serverConfigPath[] = "/opt/amnezia/openvpn/server.conf";
constexpr char caCertPath[] = "/opt/amnezia/openvpn/pki/ca.crt";
@@ -148,6 +152,7 @@ namespace amnezia
constexpr char defaultSubnetCidr[] = "24";
constexpr char defaultPort[] = "51820";
constexpr char defaultMtu[] = "1420";
constexpr char serverConfigPath[] = "/opt/amnezia/wireguard/wg0.conf";
constexpr char serverPublicKeyPath[] = "/opt/amnezia/wireguard/wireguard_server_public_key.key";
constexpr char serverPskKeyPath[] = "/opt/amnezia/wireguard/wireguard_psk.key";
@@ -163,6 +168,7 @@ namespace amnezia
namespace awg
{
constexpr char defaultPort[] = "55424";
constexpr char defaultMtu[] = "1420";
constexpr char serverConfigPath[] = "/opt/amnezia/awg/wg0.conf";
constexpr char serverPublicKeyPath[] = "/opt/amnezia/awg/wireguard_server_public_key.key";

View File

@@ -160,7 +160,6 @@
<file>ui/qml/Components/SettingsContainersListView.qml</file>
<file>ui/qml/Controls2/TextTypes/ListItemTitleType.qml</file>
<file>ui/qml/Controls2/DividerType.qml</file>
<file>ui/qml/Controls2/DrawerType.qml</file>
<file>ui/qml/Controls2/StackViewType.qml</file>
<file>ui/qml/Pages2/PageSettings.qml</file>
<file>images/controls/amnezia.svg</file>
@@ -225,5 +224,9 @@
<file>ui/qml/Pages2/PageShareFullAccess.qml</file>
<file>images/controls/close.svg</file>
<file>images/controls/search.svg</file>
<file>ui/qml/Pages2/PageProtocolWireGuardSettings.qml</file>
<file>ui/qml/Components/HomeSplitTunnelingDrawer.qml</file>
<file>images/controls/split-tunneling.svg</file>
<file>ui/qml/Controls2/DrawerType2.qml</file>
</qresource>
</RCC>

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

View File

@@ -1,9 +1,8 @@
FROM alpine:3.15
LABEL maintainer="AmneziaVPN"
ARG SS_RELEASE="v1.13.1"
ARG CLOAK_RELEASE="v2.5.5"
ARG SERVER_ARCH
ARG SS_RELEASE="v1.18.1"
ARG CLOAK_RELEASE="v2.8.0"
#Install required packages
RUN apk add --no-cache curl openvpn easy-rsa bash netcat-openbsd dumb-init rng-tools
@@ -16,20 +15,19 @@ RUN mkdir -p /opt/amnezia
RUN echo -e "#!/bin/bash\ntail -f /dev/null" > /opt/amnezia/start.sh
RUN chmod a+x /opt/amnezia/start.sh
RUN if [ $SERVER_ARCH="x86_64" ]; then CK_ARCH="amd64"; \
elif [ $SERVER_ARCH="i686" ]; then CK_ARCH="386"; \
elif [ $SERVER_ARCH="aarch64" ]; then CK_ARCH="arm64"; \
elif [ $SERVER_ARCH="arm" ]; then CK_ARCH="arm"; \
else exit -1; fi && \
curl -L https://github.com/cbeuw/Cloak/releases/download/${CLOAK_RELEASE}/ck-server-linux-${CK_ARCH}-${CLOAK_RELEASE} > /usr/bin/ck-server
RUN chmod a+x /usr/bin/ck-server
RUN SERVER_ARCH=$(uname -m) && \
if [ $SERVER_ARCH="x86_64" ]; then CK_ARCH="amd64"; \
elif [ $SERVER_ARCH="i686" ]; then CK_ARCH="386"; \
elif [ $SERVER_ARCH="aarch64" ]; then CK_ARCH="arm64"; \
elif [ $SERVER_ARCH="arm" ]; then CK_ARCH="arm"; \
else exit -1; fi && \
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 && \
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/ && \
chmod a+x /usr/bin/ssserver
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 tar -Jxvf /usr/bin/ss.tar.xz -C /usr/bin/
RUN chmod a+x /usr/bin/ssserver
# Tune network
# Tune network
RUN echo -e " \n\
fs.file-max = 51200 \n\
\n\

View File

@@ -1,8 +1,7 @@
FROM alpine:3.15
LABEL maintainer="AmneziaVPN"
ARG SS_RELEASE="v1.13.1"
ARG SERVER_ARCH
ARG SS_RELEASE="v1.18.1"
#Install required packages
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 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/;\
chmod a+x /usr/bin/ssserver;

View File

@@ -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 rm -fv
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 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 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 rm -frd /opt/amnezia

View File

@@ -1,3 +1,3 @@
sudo docker stop $CONTAINER_NAME
sudo docker rm -fv $CONTAINER_NAME
sudo docker stop $CONTAINER_NAME;\
sudo docker rm -fv $CONTAINER_NAME;\
sudo docker rmi $CONTAINER_NAME

View File

@@ -68,6 +68,7 @@ void Settings::removeServer(int index)
servers.removeAt(index);
setServersArray(servers);
emit serverRemoved(index);
}
bool Settings::editServer(int index, const QJsonObject &server)
@@ -225,9 +226,22 @@ void Settings::setSaveLogs(bool enabled)
}
}
#endif
if (enabled) {
setLogEnableDate(QDateTime::currentDateTime());
}
emit saveLogsChanged(enabled);
}
QDateTime Settings::getLogEnableDate()
{
return value("Conf/logEnableDate").toDateTime();
}
void Settings::setLogEnableDate(QDateTime date)
{
setValue("Conf/logEnableDate", date);
}
QString Settings::routeModeString(RouteMode mode) const
{
switch (mode) {
@@ -338,6 +352,7 @@ QString Settings::secondaryDns() const
void Settings::clearSettings()
{
m_settings.clearSettings();
emit settingsCleared();
}
ServerCredentials Settings::defaultServerCredentials() const

View File

@@ -100,6 +100,9 @@ public:
}
void setSaveLogs(bool enabled);
QDateTime getLogEnableDate();
void setLogEnableDate(QDateTime date);
enum RouteMode {
VpnAllSites,
VpnOnlyForwardSites,
@@ -185,12 +188,16 @@ public:
void setScreenshotsEnabled(bool enabled)
{
setValue("Conf/screenshotsEnabled", enabled);
emit screenshotsEnabledChanged(enabled);
}
void clearSettings();
signals:
void saveLogsChanged(bool enabled);
void screenshotsEnabledChanged(bool enabled);
void serverRemoved(int serverIndex);
void settingsCleared();
private:
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

File diff suppressed because it is too large Load Diff

View File

@@ -70,14 +70,17 @@ QJsonObject ApiController::fillApiPayload(const QString &protocol, const ApiCont
void ApiController::updateServerConfigFromApi()
{
QtConcurrent::run([this]() {
if (m_isConfigUpdateStarted) {
emit updateFinished(false);
return;
}
auto serverConfig = m_serversModel->getDefaultServerConfig();
auto containerConfig = serverConfig.value(config_key::containers).toArray();
bool isConfigUpdateStarted = false;
if (serverConfig.value(config_key::configVersion).toInt() && containerConfig.isEmpty()) {
emit updateStarted();
isConfigUpdateStarted = true;
m_isConfigUpdateStarted = true;
QNetworkAccessManager manager;
@@ -87,7 +90,7 @@ void ApiController::updateServerConfigFromApi()
request.setRawHeader("Authorization",
"Api-Key " + serverConfig.value(configKey::accessToken).toString().toUtf8());
QString endpoint = serverConfig.value(configKey::apiEdnpoint).toString();
request.setUrl(endpoint.replace("https", "http")); // todo remove
request.setUrl(endpoint);
QString protocol = serverConfig.value(configKey::protocol).toString();
@@ -110,6 +113,12 @@ void ApiController::updateServerConfigFromApi()
QByteArray ba = QByteArray::fromBase64(data.toUtf8(),
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
if (ba.isEmpty()) {
emit errorOccurred(errorString(ApiConfigDownloadError));
m_isConfigUpdateStarted = false;
return;
}
QByteArray ba_uncompressed = qUncompress(ba);
if (!ba_uncompressed.isEmpty()) {
ba = ba_uncompressed;
@@ -127,17 +136,21 @@ void ApiController::updateServerConfigFromApi()
auto defaultContainer = apiConfig.value(config_key::defaultContainer).toString();
serverConfig.insert(config_key::defaultContainer, defaultContainer);
m_serversModel->editServer(serverConfig);
emit m_serversModel->defaultContainerChanged(ContainerProps::containerFromString(defaultContainer));
m_serversModel->editServer(serverConfig, m_serversModel->getDefaultServerIndex());
} else {
QString err = reply->errorString();
qDebug() << QString::fromUtf8(reply->readAll());
qDebug() << reply->error();
qDebug() << err;
qDebug() << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
emit errorOccurred(errorString(ApiConfigDownloadError));
m_isConfigUpdateStarted = false;
return;
}
}
emit updateFinished(isConfigUpdateStarted);
emit updateFinished(m_isConfigUpdateStarted);
m_isConfigUpdateStarted = false;
return;
});
}
@@ -153,5 +166,5 @@ void ApiController::clearApiConfig()
serverConfig.insert(config_key::defaultContainer, ContainerProps::containerToString(DockerContainer::None));
m_serversModel->editServer(serverConfig);
m_serversModel->editServer(serverConfig, m_serversModel->getDefaultServerIndex());
}

View File

@@ -39,6 +39,8 @@ private:
QSharedPointer<ServersModel> m_serversModel;
QSharedPointer<ContainersModel> m_containersModel;
bool m_isConfigUpdateStarted = false;
};
#endif // APICONTROLLER_H

View File

@@ -25,15 +25,16 @@ ConnectionController::ConnectionController(const QSharedPointer<ServersModel> &s
void ConnectionController::openConnection()
{
if (!m_containersModel->isAnyContainerInstalled()) {
int serverIndex = m_serversModel->getDefaultServerIndex();
if (!m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) {
emit noInstalledContainers();
return;
}
int serverIndex = m_serversModel->getDefaultServerIndex();
ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex);
DockerContainer container = m_containersModel->getDefaultContainer();
DockerContainer container = qvariant_cast<DockerContainer>(m_serversModel->data(serverIndex, ServersModel::Roles::DefaultContainerRole));
const QJsonObject &containerConfig = m_containersModel->getContainerConfig(container);
if (container == DockerContainer::None) {
@@ -61,7 +62,7 @@ void ConnectionController::onConnectionStateChanged(Vpn::ConnectionState state)
m_state = state;
m_isConnected = false;
m_connectionStateText = tr("Connection...");
m_connectionStateText = tr("Connecting...");
switch (state) {
case Vpn::ConnectionState::Connected: {
m_isConnectionInProgress = false;
@@ -75,7 +76,7 @@ void ConnectionController::onConnectionStateChanged(Vpn::ConnectionState state)
}
case Vpn::ConnectionState::Reconnecting: {
m_isConnectionInProgress = true;
m_connectionStateText = tr("Reconnection...");
m_connectionStateText = tr("Reconnecting...");
break;
}
case Vpn::ConnectionState::Disconnected: {
@@ -85,7 +86,7 @@ void ConnectionController::onConnectionStateChanged(Vpn::ConnectionState state)
}
case Vpn::ConnectionState::Disconnecting: {
m_isConnectionInProgress = true;
m_connectionStateText = tr("Disconnection...");
m_connectionStateText = tr("Disconnecting...");
break;
}
case Vpn::ConnectionState::Preparing: {

View File

@@ -8,6 +8,7 @@
#include <QImage>
#include <QStandardPaths>
#include "configurators/awg_configurator.h"
#include "configurators/cloak_configurator.h"
#include "configurators/openvpn_configurator.h"
#include "configurators/shadowsocks_configurator.h"
@@ -45,7 +46,7 @@ void ExportController::generateFullAccessConfig()
{
clearPreviousConfig();
int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex();
int serverIndex = m_serversModel->getProcessedServerIndex();
QJsonObject config = m_settings->server(serverIndex);
QJsonArray containers = config.value(config_key::containers).toArray();
@@ -99,7 +100,7 @@ void ExportController::generateConnectionConfig(const QString &clientName)
{
clearPreviousConfig();
int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex();
int serverIndex = m_serversModel->getProcessedServerIndex();
ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex);
DockerContainer container = static_cast<DockerContainer>(m_containersModel->getCurrentlyProcessedContainerIndex());
@@ -155,7 +156,7 @@ void ExportController::generateOpenVpnConfig(const QString &clientName)
{
clearPreviousConfig();
int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex();
int serverIndex = m_serversModel->getProcessedServerIndex();
ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex);
DockerContainer container = static_cast<DockerContainer>(m_containersModel->getCurrentlyProcessedContainerIndex());
@@ -193,7 +194,7 @@ void ExportController::generateWireGuardConfig(const QString &clientName)
{
clearPreviousConfig();
int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex();
int serverIndex = m_serversModel->getProcessedServerIndex();
ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex);
DockerContainer container = static_cast<DockerContainer>(m_containersModel->getCurrentlyProcessedContainerIndex());
@@ -228,11 +229,50 @@ void ExportController::generateWireGuardConfig(const QString &clientName)
emit exportConfigChanged();
}
void ExportController::generateAwgConfig(const QString &clientName)
{
clearPreviousConfig();
int serverIndex = m_serversModel->getProcessedServerIndex();
ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex);
DockerContainer container = static_cast<DockerContainer>(m_containersModel->getCurrentlyProcessedContainerIndex());
QJsonObject containerConfig = m_containersModel->getContainerConfig(container);
containerConfig.insert(config_key::container, ContainerProps::containerToString(container));
QString clientId;
ErrorCode errorCode = ErrorCode::NoError;
QString config = m_configurator->awgConfigurator->genAwgConfig(credentials, container, containerConfig,
clientId, &errorCode);
if (errorCode) {
emit exportErrorOccurred(errorString(errorCode));
return;
}
config = m_configurator->processConfigWithExportSettings(serverIndex, container, Proto::Awg, config);
auto configJson = QJsonDocument::fromJson(config.toUtf8()).object();
QStringList lines = configJson.value(config_key::config).toString().replace("\r", "").split("\n");
for (const QString &line : lines) {
m_config.append(line + "\n");
}
qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(m_config.toUtf8(), qrcodegen::QrCode::Ecc::LOW);
m_qrCodes << svgToBase64(QString::fromStdString(toSvgString(qr, 1)));
errorCode = m_clientManagementModel->appendClient(clientId, clientName, container, credentials);
if (errorCode) {
emit exportErrorOccurred(errorString(errorCode));
return;
}
emit exportConfigChanged();
}
void ExportController::generateShadowSocksConfig()
{
clearPreviousConfig();
int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex();
int serverIndex = m_serversModel->getProcessedServerIndex();
ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex);
DockerContainer container = static_cast<DockerContainer>(m_containersModel->getCurrentlyProcessedContainerIndex());
@@ -268,7 +308,7 @@ void ExportController::generateCloakConfig()
{
clearPreviousConfig();
int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex();
int serverIndex = m_serversModel->getProcessedServerIndex();
ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex);
DockerContainer container = static_cast<DockerContainer>(m_containersModel->getCurrentlyProcessedContainerIndex());
@@ -328,7 +368,7 @@ void ExportController::updateClientManagementModel(const DockerContainer contain
void ExportController::revokeConfig(const int row, const DockerContainer container, ServerCredentials credentials)
{
ErrorCode errorCode = m_clientManagementModel->revokeClient(row, container, credentials,
m_serversModel->getCurrentlyProcessedServerIndex());
m_serversModel->getProcessedServerIndex());
if (errorCode != ErrorCode::NoError) {
emit exportErrorOccurred(errorString(errorCode));
}
@@ -378,4 +418,6 @@ void ExportController::clearPreviousConfig()
m_config.clear();
m_nativeConfigString.clear();
m_qrCodes.clear();
emit exportConfigChanged();
}

View File

@@ -34,6 +34,7 @@ public slots:
void generateConnectionConfig(const QString &clientName);
void generateOpenVpnConfig(const QString &clientName);
void generateWireGuardConfig(const QString &clientName);
void generateAwgConfig(const QString &clientName);
void generateShadowSocksConfig();
void generateCloakConfig();

View File

@@ -18,7 +18,9 @@ namespace
enum class ConfigTypes {
Amnezia,
OpenVpn,
WireGuard
WireGuard,
Backup,
Invalid
};
ConfigTypes checkConfigFormat(const QString &config)
@@ -32,15 +34,23 @@ namespace
const QString wireguardConfigPatternSectionInterface = "[Interface]";
const QString wireguardConfigPatternSectionPeer = "[Peer]";
if (config.contains(openVpnConfigPatternCli)
&& (config.contains(openVpnConfigPatternProto1) || config.contains(openVpnConfigPatternProto2))
&& (config.contains(openVpnConfigPatternDriver1) || config.contains(openVpnConfigPatternDriver2))) {
const QString amneziaConfigPattern = "containers";
const QString amneziaFreeConfigPattern = "api_key";
const QString backupPattern = "Servers/serversList";
if (config.contains(backupPattern)) {
return ConfigTypes::Backup;
} else if (config.contains(amneziaConfigPattern) || config.contains(amneziaFreeConfigPattern)) {
return ConfigTypes::Amnezia;
} else if (config.contains(openVpnConfigPatternCli)
&& (config.contains(openVpnConfigPatternProto1) || config.contains(openVpnConfigPatternProto2))
&& (config.contains(openVpnConfigPatternDriver1) || config.contains(openVpnConfigPatternDriver2))) {
return ConfigTypes::OpenVpn;
} else if (config.contains(wireguardConfigPatternSectionInterface)
&& config.contains(wireguardConfigPatternSectionPeer)) {
return ConfigTypes::WireGuard;
}
return ConfigTypes::Amnezia;
return ConfigTypes::Invalid;
}
#if defined Q_OS_ANDROID
@@ -58,34 +68,65 @@ ImportController::ImportController(const QSharedPointer<ServersModel> &serversMo
#endif
}
void ImportController::extractConfigFromFile(const QString &fileName)
bool ImportController::extractConfigFromFile(const QString &fileName)
{
QFile file(fileName);
if (file.open(QIODevice::ReadOnly)) {
QString data = file.readAll();
extractConfigFromData(data);
m_configFileName = QFileInfo(file.fileName()).fileName();
return extractConfigFromData(data);
}
emit importErrorOccurred(tr("Unable to open file"), false);
return false;
}
void ImportController::extractConfigFromData(QString data)
bool ImportController::extractConfigFromData(QString data)
{
auto configFormat = checkConfigFormat(data);
if (configFormat == ConfigTypes::OpenVpn) {
m_config = extractOpenVpnConfig(data);
} else if (configFormat == ConfigTypes::WireGuard) {
m_config = extractWireGuardConfig(data);
} else {
m_config = extractAmneziaConfig(data);
}
}
QString config = data;
auto configFormat = checkConfigFormat(config);
if (configFormat == ConfigTypes::Invalid) {
data.replace("vpn://", "");
QByteArray ba =
QByteArray::fromBase64(data.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
QByteArray ba_uncompressed = qUncompress(ba);
if (!ba_uncompressed.isEmpty()) {
ba = ba_uncompressed;
}
void ImportController::extractConfigFromCode(QString code)
{
m_config = extractAmneziaConfig(code);
m_configFileName = "";
config = ba;
configFormat = checkConfigFormat(config);
}
switch (configFormat) {
case ConfigTypes::OpenVpn: {
m_config = extractOpenVpnConfig(config);
return m_config.empty() ? false : true;
}
case ConfigTypes::WireGuard: {
m_config = extractWireGuardConfig(config);
return m_config.empty() ? false : true;
}
case ConfigTypes::Amnezia: {
m_config = QJsonDocument::fromJson(config.toUtf8()).object();
return m_config.empty() ? false : true;
}
case ConfigTypes::Backup: {
if (!m_serversModel->getServersCount()) {
emit restoreAppConfig(config.toUtf8());
} else {
emit importErrorOccurred(tr("Invalid configuration file"), false);
}
break;
}
case ConfigTypes::Invalid: {
emit importErrorOccurred(tr("Invalid configuration file"), false);
break;
}
}
return false;
}
bool ImportController::extractConfigFromQr(const QByteArray &data)
@@ -139,28 +180,13 @@ void ImportController::importConfig()
} else {
qDebug() << "Failed to import profile";
qDebug().noquote() << QJsonDocument(m_config).toJson();
emit importErrorOccurred(errorString(ErrorCode::ImportInvalidConfigError));
emit importErrorOccurred(errorString(ErrorCode::ImportInvalidConfigError), false);
}
m_config = {};
m_configFileName.clear();
}
QJsonObject ImportController::extractAmneziaConfig(QString &data)
{
data.replace("vpn://", "");
QByteArray ba = QByteArray::fromBase64(data.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
QByteArray ba_uncompressed = qUncompress(ba);
if (!ba_uncompressed.isEmpty()) {
ba = ba_uncompressed;
}
QJsonObject config = QJsonDocument::fromJson(ba).object();
return config;
}
QJsonObject ImportController::extractOpenVpnConfig(const QString &data)
{
QJsonObject openVpnConfig;
@@ -229,8 +255,9 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data)
if (hostNameAndPortMatch.hasCaptured(1)) {
hostName = hostNameAndPortMatch.captured(1);
} else {
qDebug() << "Failed to import profile";
emit importErrorOccurred(errorString(ErrorCode::ImportInvalidConfigError));
qDebug() << "Key parameter 'Endpoint' is missing";
emit importErrorOccurred(errorString(ErrorCode::ImportInvalidConfigError), false);
return QJsonObject();
}
if (hostNameAndPortMatch.hasCaptured(2)) {
@@ -242,10 +269,11 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data)
lastConfig[config_key::hostName] = hostName;
lastConfig[config_key::port] = port.toInt();
// if (!configMap.value("PrivateKey").isEmpty() && !configMap.value("Address").isEmpty()
// && !configMap.value("PresharedKey").isEmpty() && !configMap.value("PublicKey").isEmpty()) {
if (!configMap.value("PrivateKey").isEmpty() && !configMap.value("Address").isEmpty()
&& !configMap.value("PublicKey").isEmpty()) {
lastConfig[config_key::client_priv_key] = configMap.value("PrivateKey");
lastConfig[config_key::client_ip] = configMap.value("Address");
if (!configMap.value("PresharedKey").isEmpty()) {
lastConfig[config_key::psk_key] = configMap.value("PresharedKey");
} else if (!configMap.value("PreSharedKey").isEmpty()) {
@@ -253,11 +281,15 @@ QJsonObject ImportController::extractWireGuardConfig(const QString &data)
}
lastConfig[config_key::server_pub_key] = configMap.value("PublicKey");
// } else {
// qDebug() << "Failed to import profile";
// emit importErrorOccurred(errorString(ErrorCode::ImportInvalidConfigError));
// return QJsonObject();
// }
} else {
qDebug() << "One of the key parameters is missing (PrivateKey, Address, PublicKey)";
emit importErrorOccurred(errorString(ErrorCode::ImportInvalidConfigError), false);
return QJsonObject();
}
if (!configMap.value("MTU").isEmpty()) {
lastConfig[config_key::mtu] = configMap.value("MTU");
}
QJsonArray allowedIpsJsonArray = QJsonArray::fromStringList(configMap.value("AllowedIPs").split(","));

View File

@@ -18,9 +18,8 @@ public:
public slots:
void importConfig();
void extractConfigFromFile(const QString &fileName);
void extractConfigFromData(QString data);
void extractConfigFromCode(QString code);
bool extractConfigFromFile(const QString &fileName);
bool extractConfigFromData(QString data);
bool extractConfigFromQr(const QByteArray &data);
QString getConfig();
QString getConfigFileName();
@@ -39,12 +38,13 @@ public slots:
signals:
void importFinished();
void importErrorOccurred(const QString &errorMessage, bool goToPageHome = false);
void importErrorOccurred(const QString &errorMessage, bool goToPageHome);
void qrDecodingFinished();
void restoreAppConfig(const QByteArray &data);
private:
QJsonObject extractAmneziaConfig(QString &data);
QJsonObject extractOpenVpnConfig(const QString &data);
QJsonObject extractWireGuardConfig(const QString &data);

View File

@@ -10,6 +10,8 @@
#include "core/errorstrings.h"
#include "core/controllers/serverController.h"
#include "utilities.h"
#include "ui/models/protocols/awgConfigModel.h"
#include "ui/models/protocols/wireguardConfigModel.h"
namespace
{
@@ -176,7 +178,7 @@ void InstallController::installServer(DockerContainer container, QJsonObject &co
void InstallController::installContainer(DockerContainer container, QJsonObject &config)
{
int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex();
int serverIndex = m_serversModel->getProcessedServerIndex();
ServerCredentials serverCredentials =
qvariant_cast<ServerCredentials>(m_serversModel->data(serverIndex, ServersModel::Roles::CredentialsRole));
@@ -238,7 +240,7 @@ bool InstallController::isServerAlreadyExists()
void InstallController::scanServerForInstalledContainers()
{
int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex();
int serverIndex = m_serversModel->getProcessedServerIndex();
ServerCredentials serverCredentials =
qvariant_cast<ServerCredentials>(m_serversModel->data(serverIndex, ServersModel::Roles::CredentialsRole));
@@ -267,24 +269,28 @@ void InstallController::scanServerForInstalledContainers()
void InstallController::updateContainer(QJsonObject config)
{
int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex();
int serverIndex = m_serversModel->getProcessedServerIndex();
ServerCredentials serverCredentials =
qvariant_cast<ServerCredentials>(m_serversModel->data(serverIndex, ServersModel::Roles::CredentialsRole));
const DockerContainer container = ContainerProps::containerFromString(config.value(config_key::container).toString());
QJsonObject oldContainerConfig = m_containersModel->getContainerConfig(container);
ErrorCode errorCode = ErrorCode::NoError;
ServerController serverController(m_settings);
connect(&serverController, &ServerController::serverIsBusy, this, &InstallController::serverIsBusy);
connect(this, &InstallController::cancelInstallation, &serverController, &ServerController::cancelInstallation);
if (isUpdateDockerContainerRequired(container, oldContainerConfig, config)) {
ServerController serverController(m_settings);
connect(&serverController, &ServerController::serverIsBusy, this, &InstallController::serverIsBusy);
connect(this, &InstallController::cancelInstallation, &serverController, &ServerController::cancelInstallation);
errorCode = serverController.updateContainer(serverCredentials, container, oldContainerConfig, config);
}
auto errorCode = serverController.updateContainer(serverCredentials, container, oldContainerConfig, config);
if (errorCode == ErrorCode::NoError) {
m_serversModel->updateContainerConfig(container, config);
m_protocolModel->updateModel(config);
if ((serverIndex == m_serversModel->getDefaultServerIndex())
&& (container == m_containersModel->getDefaultContainer())) {
auto defaultContainer = qvariant_cast<DockerContainer>(m_serversModel->data(serverIndex, ServersModel::Roles::DefaultContainerRole));
if ((serverIndex == m_serversModel->getDefaultServerIndex()) && (container == defaultContainer)) {
emit currentContainerUpdated();
} else {
emit updateContainerFinished(tr("Settings updated successfully"));
@@ -296,27 +302,27 @@ void InstallController::updateContainer(QJsonObject config)
emit installationErrorOccurred(errorString(errorCode));
}
void InstallController::rebootCurrentlyProcessedServer()
void InstallController::rebootProcessedServer()
{
int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex();
int serverIndex = m_serversModel->getProcessedServerIndex();
QString serverName = m_serversModel->data(serverIndex, ServersModel::Roles::NameRole).toString();
m_serversModel->rebootServer();
emit rebootCurrentlyProcessedServerFinished(tr("Server '%1' was rebooted").arg(serverName));
emit rebootProcessedServerFinished(tr("Server '%1' was rebooted").arg(serverName));
}
void InstallController::removeCurrentlyProcessedServer()
void InstallController::removeProcessedServer()
{
int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex();
int serverIndex = m_serversModel->getProcessedServerIndex();
QString serverName = m_serversModel->data(serverIndex, ServersModel::Roles::NameRole).toString();
m_serversModel->removeServer();
emit removeCurrentlyProcessedServerFinished(tr("Server '%1' was removed").arg(serverName));
emit removeProcessedServerFinished(tr("Server '%1' was removed").arg(serverName));
}
void InstallController::removeAllContainers()
{
int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex();
int serverIndex = m_serversModel->getProcessedServerIndex();
QString serverName = m_serversModel->data(serverIndex, ServersModel::Roles::NameRole).toString();
ErrorCode errorCode = m_serversModel->removeAllContainers();
@@ -329,7 +335,7 @@ void InstallController::removeAllContainers()
void InstallController::removeCurrentlyProcessedContainer()
{
int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex();
int serverIndex = m_serversModel->getProcessedServerIndex();
QString serverName = m_serversModel->data(serverIndex, ServersModel::Roles::NameRole).toString();
int container = m_containersModel->getCurrentlyProcessedContainerIndex();
@@ -377,7 +383,7 @@ void InstallController::mountSftpDrive(const QString &port, const QString &passw
QString mountPath;
QString cmd;
int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex();
int serverIndex = m_serversModel->getProcessedServerIndex();
ServerCredentials serverCredentials =
qvariant_cast<ServerCredentials>(m_serversModel->data(serverIndex, ServersModel::Roles::CredentialsRole));
QString hostname = serverCredentials.hostName;
@@ -514,3 +520,29 @@ void InstallController::addEmptyServer()
emit installServerFinished(tr("Server added successfully"));
}
bool InstallController::isUpdateDockerContainerRequired(const DockerContainer container, const QJsonObject &oldConfig, const QJsonObject &newConfig)
{
Proto mainProto = ContainerProps::defaultProtocol(container);
const QJsonObject &oldProtoConfig = oldConfig.value(ProtocolProps::protoToString(mainProto)).toObject();
const QJsonObject &newProtoConfig = newConfig.value(ProtocolProps::protoToString(mainProto)).toObject();
if (container == DockerContainer::Awg) {
const AwgConfig oldConfig(oldProtoConfig);
const AwgConfig newConfig(newProtoConfig);
if (!oldConfig.hasEqualServerSettings(newConfig)) {
return true;
}
} else if (container == DockerContainer::WireGuard) {
const WgConfig oldConfig(oldProtoConfig);
const WgConfig newConfig(newProtoConfig);
if (!oldConfig.hasEqualServerSettings(newConfig)) {
return true;
}
}
return false;
}

View File

@@ -30,8 +30,8 @@ public slots:
void updateContainer(QJsonObject config);
void removeCurrentlyProcessedServer();
void rebootCurrentlyProcessedServer();
void removeProcessedServer();
void rebootProcessedServer();
void removeAllContainers();
void removeCurrentlyProcessedContainer();
@@ -54,8 +54,8 @@ signals:
void scanServerFinished(bool isInstalledContainerFound);
void rebootCurrentlyProcessedServerFinished(const QString &finishedMessage);
void removeCurrentlyProcessedServerFinished(const QString &finishedMessage);
void rebootProcessedServerFinished(const QString &finishedMessage);
void removeProcessedServerFinished(const QString &finishedMessage);
void removeAllContainersFinished(const QString &finishedMessage);
void removeCurrentlyProcessedContainerFinished(const QString &finishedMessage);
@@ -76,6 +76,8 @@ private:
void installContainer(DockerContainer container, QJsonObject &config);
bool isServerAlreadyExists();
bool isUpdateDockerContainerRequired(const DockerContainer container, const QJsonObject &oldConfig, const QJsonObject &newConfig);
QSharedPointer<ServersModel> m_serversModel;
QSharedPointer<ContainersModel> m_containersModel;
QSharedPointer<ProtocolsModel> m_protocolModel;

View File

@@ -77,7 +77,16 @@ void PageController::closeWindow()
void PageController::keyPressEvent(Qt::Key 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;
}
}
@@ -118,42 +127,12 @@ void PageController::showOnStartup()
}
}
void PageController::updateDrawerRootPage(PageLoader::PageEnum page)
{
m_drawerLayer = 0;
m_currentRootPage = page;
}
void PageController::goToDrawerRootPage()
{
m_drawerLayer = 0;
emit showTopCloseButton(false);
emit forceCloseDrawer();
}
void PageController::drawerOpen()
{
m_drawerLayer = m_drawerLayer + 1;
emit showTopCloseButton(true);
}
void PageController::drawerClose()
{
m_drawerLayer = m_drawerLayer -1;
if (m_drawerLayer <= 0) {
emit showTopCloseButton(false);
m_drawerLayer = 0;
}
}
bool PageController::isTriggeredByConnectButton()
{
return m_isTriggeredByConnectButton;
}
void PageController::setTriggeredBtConnectButton(bool trigger)
void PageController::setTriggeredByConnectButton(bool trigger)
{
m_isTriggeredByConnectButton = trigger;
}
@@ -162,3 +141,15 @@ void PageController::closeApplication()
{
qApp->quit();
}
void PageController::setDrawerDepth(const int depth)
{
if (depth >= 0) {
m_drawerDepth = depth;
}
}
int PageController::getDrawerDepth()
{
return m_drawerDepth;
}

View File

@@ -82,16 +82,14 @@ public slots:
void showOnStartup();
void updateDrawerRootPage(PageLoader::PageEnum page);
void goToDrawerRootPage();
void drawerOpen();
void drawerClose();
bool isTriggeredByConnectButton();
void setTriggeredBtConnectButton(bool trigger);
void setTriggeredByConnectButton(bool trigger);
void closeApplication();
void setDrawerDepth(const int depth);
int getDrawerDepth();
signals:
void goToPage(PageLoader::PageEnum page, bool slide = true);
void goToStartPage();
@@ -110,7 +108,7 @@ signals:
void showNotificationMessage(const QString &message);
void showBusyIndicator(bool visible);
void enableTabBar(bool enabled);
void disableControls(bool disabled);
void hideMainWindow();
void raiseMainWindow();
@@ -118,18 +116,17 @@ signals:
void showPassphraseRequestDrawer();
void passphraseRequestDrawerClosed(QString passphrase);
void showTopCloseButton(bool visible);
void forceCloseDrawer();
void escapePressed();
void closeTopDrawer();
private:
QSharedPointer<ServersModel> m_serversModel;
std::shared_ptr<Settings> m_settings;
PageLoader::PageEnum m_currentRootPage;
int m_drawerLayer;
bool m_isTriggeredByConnectButton;
int m_drawerDepth = 0;
};
#endif // PAGECONTROLLER_H

View File

@@ -7,9 +7,7 @@
#include "ui/qautostart.h"
#include "version.h"
#ifdef Q_OS_ANDROID
#include "platforms/android/android_utils.h"
#include "platforms/android/android_controller.h"
#include <QJniObject>
#endif
#ifdef Q_OS_IOS
@@ -28,21 +26,8 @@ SettingsController::SettingsController(const QSharedPointer<ServersModel> &serve
m_sitesModel(sitesModel),
m_settings(settings)
{
m_appVersion = QString("%1: %2 (%3)").arg(tr("Software version"), QString(APP_VERSION), __DATE__);
#ifdef Q_OS_ANDROID
if (!m_settings->isScreenshotsEnabled()) {
// Set security screen for Android app
AndroidUtils::runOnAndroidThreadSync([]() {
QJniObject activity = AndroidUtils::getActivity();
QJniObject window = activity.callObjectMethod("getWindow", "()Landroid/view/Window;");
if (window.isValid()) {
const int FLAG_SECURE = 8192;
window.callMethod<void>("addFlags", "(I)V", FLAG_SECURE);
}
});
}
#endif
m_appVersion = QString("%1 (%2, %3)").arg(QString(APP_VERSION), __DATE__, GIT_COMMIT_HASH);
checkIfNeedDisableLogs();
}
void SettingsController::toggleAmneziaDns(bool enable)
@@ -87,8 +72,11 @@ void SettingsController::toggleLogging(bool enable)
{
m_settings->setSaveLogs(enable);
#ifdef Q_OS_IOS
AmneziaVPN::toggleLogging(enable);
AmneziaVPN::toggleLogging(enable);
#endif
if (enable == true) {
checkIfNeedDisableLogs();
}
emit loggingStateChanged();
}
@@ -129,6 +117,11 @@ void SettingsController::restoreAppConfig(const QString &fileName)
QByteArray data = file.readAll();
restoreAppConfigFromData(data);
}
void SettingsController::restoreAppConfigFromData(const QByteArray &data)
{
bool ok = m_settings->restoreAppConfig(data);
if (ok) {
m_serversModel->resetModel();
@@ -152,7 +145,12 @@ void SettingsController::clearSettings()
m_languageModel->changeLanguage(
static_cast<LanguageSettings::AvailableLanguageEnum>(m_languageModel->getCurrentLanguageIndex()));
m_sitesModel->setRouteMode(Settings::RouteMode::VpnAllSites);
emit changeSettingsFinished(tr("All settings have been reset to default values"));
#ifdef Q_OS_IOS
AmneziaVPN::clearSettings();
#endif
}
void SettingsController::clearCachedProfiles()
@@ -199,19 +197,6 @@ bool SettingsController::isScreenshotsEnabled()
void SettingsController::toggleScreenshotsEnabled(bool enable)
{
m_settings->setScreenshotsEnabled(enable);
#ifdef Q_OS_ANDROID
std::string command = enable ? "clearFlags" : "addFlags";
// Set security screen for Android app
AndroidUtils::runOnAndroidThreadSync([&command]() {
QJniObject activity = AndroidUtils::getActivity();
QJniObject window = activity.callObjectMethod("getWindow", "()Landroid/view/Window;");
if (window.isValid()) {
const int FLAG_SECURE = 8192;
window.callMethod<void>(command.c_str(), "(I)V", FLAG_SECURE);
}
});
#endif
}
bool SettingsController::isCameraPresent()
@@ -224,3 +209,13 @@ bool SettingsController::isCameraPresent()
return false;
#endif
}
void SettingsController::checkIfNeedDisableLogs()
{
m_loggingDisableDate = m_settings->getLogEnableDate().addDays(14);
if (m_loggingDisableDate <= QDateTime::currentDateTime()) {
toggleLogging(false);
clearLogs();
emit loggingDisableByWatcher();
}
}

View File

@@ -41,6 +41,7 @@ public slots:
void backupAppConfig(const QString &fileName);
void restoreAppConfig(const QString &fileName);
void restoreAppConfigFromData(const QByteArray &data);
QString getAppVersion();
@@ -76,6 +77,8 @@ signals:
void amneziaDnsToggled(bool enable);
void loggingDisableByWatcher();
private:
QSharedPointer<ServersModel> m_serversModel;
QSharedPointer<ContainersModel> m_containersModel;
@@ -84,6 +87,10 @@ private:
std::shared_ptr<Settings> m_settings;
QString m_appVersion;
QDateTime m_loggingDisableDate;
void checkIfNeedDisableLogs();
};
#endif // SETTINGSCONTROLLER_H

View File

@@ -92,11 +92,11 @@ QString SystemController::getFileName(const QString &acceptLabel, const QString
mainFileDialog->setProperty("acceptLabel", QVariant::fromValue(acceptLabel));
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("isSaveMode", QVariant::fromValue(isSaveMode));
if (!selectedFile.isEmpty()) {
mainFileDialog->setProperty("selectedFile", QVariant::fromValue(QUrl(selectedFile)));
}
QMetaObject::invokeMethod(mainFileDialog, "open");
bool isFileDialogAccepted = false;

View File

@@ -39,7 +39,6 @@ QVariant ContainersModel::data(const QModelIndex &index, int role) const
case EasySetupOrderRole: return ContainerProps::easySetupOrder(container);
case IsInstalledRole: return m_containers.contains(container);
case IsCurrentlyProcessedRole: return container == static_cast<DockerContainer>(m_currentlyProcessedContainerIndex);
case IsDefaultRole: return container == m_defaultContainerIndex;
case IsSupportedRole: return ContainerProps::isSupportedByCurrentPlatform(container);
case IsShareableRole: return ContainerProps::isShareable(container);
}
@@ -64,18 +63,6 @@ void ContainersModel::updateModel(const QJsonArray &containers)
endResetModel();
}
void ContainersModel::setDefaultContainer(const int containerIndex)
{
m_defaultContainerIndex = static_cast<DockerContainer>(containerIndex);
emit dataChanged(index(containerIndex, 0), index(containerIndex, 0));
}
DockerContainer ContainersModel::getDefaultContainer()
{
return m_defaultContainerIndex;
}
void ContainersModel::setCurrentlyProcessedContainerIndex(int index)
{
m_currentlyProcessedContainerIndex = index;
@@ -96,20 +83,6 @@ QJsonObject ContainersModel::getContainerConfig(const int containerIndex)
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> roles;
@@ -127,7 +100,6 @@ QHash<int, QByteArray> ContainersModel::roleNames() const
roles[IsInstalledRole] = "isInstalled";
roles[IsCurrentlyProcessedRole] = "isCurrentlyProcessed";
roles[IsDefaultRole] = "isDefault";
roles[IsSupportedRole] = "isSupported";
roles[IsShareableRole] = "isShareable";
return roles;

View File

@@ -42,9 +42,6 @@ public:
public slots:
void updateModel(const QJsonArray &containers);
DockerContainer getDefaultContainer();
void setDefaultContainer(const int containerIndex);
void setCurrentlyProcessedContainerIndex(int containerIndex);
int getCurrentlyProcessedContainerIndex();
@@ -52,20 +49,16 @@ public slots:
QJsonObject getContainerConfig(const int containerIndex);
bool isAnyContainerInstalled();
protected:
QHash<int, QByteArray> roleNames() const override;
signals:
void defaultContainerChanged();
void containersModelUpdated();
private:
QMap<DockerContainer, QJsonObject> m_containers;
int m_currentlyProcessedContainerIndex;
DockerContainer m_defaultContainerIndex;
};
#endif // CONTAINERS_MODEL_H

View File

@@ -45,6 +45,8 @@ QString LanguageModel::getLocalLanguageName(const LanguageSettings::AvailableLan
case LanguageSettings::AvailableLanguageEnum::Russian: strLanguage = "Русский"; break;
case LanguageSettings::AvailableLanguageEnum::China_cn: strLanguage = "\347\256\200\344\275\223\344\270\255\346\226\207"; break;
case LanguageSettings::AvailableLanguageEnum::Persian: strLanguage = "فارسی"; break;
case LanguageSettings::AvailableLanguageEnum::Arabic: strLanguage = "العربية"; break;
case LanguageSettings::AvailableLanguageEnum::Burmese: strLanguage = "မြန်မာဘာသာ"; break;
default:
break;
}
@@ -59,6 +61,8 @@ void LanguageModel::changeLanguage(const LanguageSettings::AvailableLanguageEnum
case LanguageSettings::AvailableLanguageEnum::Russian: emit updateTranslations(QLocale::Russian); break;
case LanguageSettings::AvailableLanguageEnum::China_cn: emit updateTranslations(QLocale::Chinese); break;
case LanguageSettings::AvailableLanguageEnum::Persian: emit updateTranslations(QLocale::Persian); break;
case LanguageSettings::AvailableLanguageEnum::Arabic: emit updateTranslations(QLocale::Arabic); break;
case LanguageSettings::AvailableLanguageEnum::Burmese: emit updateTranslations(QLocale::Burmese); break;
default: emit updateTranslations(QLocale::English); break;
}
}
@@ -71,10 +75,21 @@ int LanguageModel::getCurrentLanguageIndex()
case QLocale::Russian: return static_cast<int>(LanguageSettings::AvailableLanguageEnum::Russian); break;
case QLocale::Chinese: return static_cast<int>(LanguageSettings::AvailableLanguageEnum::China_cn); break;
case QLocale::Persian: return static_cast<int>(LanguageSettings::AvailableLanguageEnum::Persian); break;
case QLocale::Arabic: return static_cast<int>(LanguageSettings::AvailableLanguageEnum::Arabic); break;
case QLocale::Burmese: return static_cast<int>(LanguageSettings::AvailableLanguageEnum::Burmese); break;
default: return static_cast<int>(LanguageSettings::AvailableLanguageEnum::English); break;
}
}
int LanguageModel::getLineHeightAppend()
{
int langIndex = getCurrentLanguageIndex();
switch (langIndex) {
case 5: return 10; break; // Burmese
default: return 0; break;
}
}
QString LanguageModel::getCurrentLanguageName()
{
return m_availableLanguages[getCurrentLanguageIndex()].name;

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