Compare commits

...

65 Commits

Author SHA1 Message Date
Mitternacht822 3e719aac56 fix: fixed incorrect lines 2025-12-03 15:23:57 +04:00
Mitternacht822 f87925c819 fix: fixed empty spaces 2025-12-03 14:59:59 +04:00
Stageddat 63c6d7a77b fix: fix grammar 2025-12-03 09:46:45 +01:00
Stageddat 90ef4ec3de feat: add spanish translations to client 2025-12-03 09:43:59 +01:00
Stageddat eb7bfc2815 feat: add spanish translation 2025-12-03 09:32:58 +01:00
AnhTVc ac77b4ee75 feat: add network status check for awg/wg protocol (#1894)
* Add network  status check for AWG/WG protocol

* Use service for PingSender

* Cleanup unused code

* Use networkchecker for all protocols

* fix android build

* add delay for ping checker stop

* handle for interafe problems on windows

* Restart IpcClient after OS suspend

* Add DBus network checker for Linux

* Use ping check for tun interfce

* Windows suspend mode handler

* MacOS suspend mode handler draft

* Add delay for Linux wakeup reconnect

* Add delay for Linux wakeup reconnect

* Fix macOS  wakeup/sleep prob

Fix macOS not receiving wakeup/sleep events

* fix done

* Update deploy.yml

fix CICD

* Update vpnconnection.cpp

update fix build CICD

* Update vpnconnection.cpp

update fix build cicd macos

* Update deploy.yml

fix  CICD build macos

* Update deploy.yml

fix CICD macos

* feat: implement SCP write buffer, improve network check and refactor macOS OpenGL support

* feat: add tunnel addresses updated signal and handle network check based on gateway and local address availability

* refactor: improve IpcClient connection handling and instance management

* fix: scp revert.

* fix: cmake reverted.

* fix: submodules updated

---------

Co-authored-by: Mykola Baibuz <mykola.baibuz@gmail.com>
Co-authored-by: Yaroslav Yashin <yaroslav.yashin@gmail.com>
Co-authored-by: vkamn <vk@amnezia.org>
2025-12-02 12:46:24 +08:00
NickVs2015 fbf652f818 feat: add vless string on sharing screen (#1999)
* feat: add vless config string and serialization

* feat: add vless config string and serialization
2025-12-02 12:09:04 +08:00
vkamn bbbf4891e6 fix: fixed define name for linux os signal handler (#2030) 2025-12-02 11:14:09 +08:00
MrMirDan 20d005d66c fix: clear file name to remove header (#1984)
* fix: clear file name to remove header

* update: clear on signal

* removed uneccessary function

* fix: clear filename on invalid config type

---------

Co-authored-by: vkamn <vk@amnezia.org>
2025-12-02 11:13:26 +08:00
MrMirDan c81ae2b060 fix: update or delete news on newsModel update (#2007)
* fix: update or delete news on newsModel update

* update: changed check for news editing

* update: changed news edit updating

* update: changed news model updating method

* chore: add rich text support for news page

---------

Co-authored-by: vkamn <vk@amnezia.org>
2025-12-01 20:23:14 +08:00
Yaroslav 105c42db1c fix: ipc call in macos ne (#1986) 2025-12-01 10:54:42 +08:00
Mykola Baibuz 89818ff63d fix: app freeze on quit (#1804)
* fix: app freeze on quit

* fix: typo in VpnConnection destructor

* add trace info

* add more trace info

* set timelimit for flushDns

* Refactor IpcClient::Interface access logic

* cleanup unused variable

* cleanup trace info

* fix: remove second disconnect from VPN on app close

* this object will be deleted at app close

* Don't terminate VPN thread on Linux

* Revert "Don't terminate VPN thread on Linux"

This reverts commit 20e4ea2d4a.

* disconnect all signals from vpnconnection on exit

* add interruption request on vpnConnectionThread

* use checktimer only for iOS

* disconnect all signals from vpnconnection on exit

* disconnect signals on exit before VPN disconnect

* add disconnectSlots method

* fix: add allow traffic rules on killswitch disable

* wait for response from service before object destroy

* change disconnect from vpn order

* add delay for connection close

* change disconnect method order

* use stop method for protocol disconnecect

* change disconnect method order

* allow dns traffic after app close

* delete tun on disconnect

---------

Co-authored-by: vkamn <vk@amnezia.org>
2025-12-01 10:49:16 +08:00
vkamn 414c422177 feat: added os signal handler (#2029) 2025-12-01 10:45:06 +08:00
NickVs2015 b39ac8556c feat: add right artifact name (#2018) 2025-11-28 12:08:38 +08:00
MrMirDan 5e1742262d fix: eye icon (#1985)
Co-authored-by: vkamn <vk@amnezia.org>
2025-11-28 11:00:53 +08:00
VoyNaLunu 5a07a1274f fix: GetBestRoute always returning 1231 error (#1981)
* fix GetBestRoute always returning 1231 error

* revert some changes because fix turned out to be simpler
2025-11-26 12:46:55 +08:00
MrMirDan 7b8ff1fd6e fix: checked format after changing protocol (#1937)
* fix: checked format after changing protocol

* update: improved some lines

* fix(ui): restore checkmark for connection format after switching protocol

* fix: correct a typo

* fix(ui): escape regex in client search filter

* refactor: removed redundant lines

---------

Co-authored-by: Mitternacht822 <sb@amnezia.org>
2025-11-26 12:07:24 +08:00
MrMirDan c7221832e0 fix: users search field clears on 'x' button or 'escape' key clicked (#1920) 2025-11-26 11:57:28 +08:00
NickVs2015 eb7d031c7d fix: clear qt cache on start app (#2008)
* Fix/ Cache clear Android

* Fix: Clear cache on start app

* chore: bump version

---------

Co-authored-by: vkamn <vk@amnezia.org>
2025-11-26 11:47:50 +08:00
vkamn 3b3a0aaceb chore: bump version (#1997) 2025-11-18 00:22:58 +08:00
vkamn 01ec79b7d5 fix: news fetch (#1994)
* fix: fixed news nested qml call

* feat: async proxy bypass
2025-11-18 00:21:02 +08:00
vkamn 3d6339e2dd chore: bump version (#1989) 2025-11-14 13:59:47 +08:00
NickVs2015 b4d78d865a fix: fix android crash (#1988) 2025-11-14 13:57:52 +08:00
NickVs2015 b53cdcff08 fix: fix self-hosted TextFields and Keyboard reset issue (#1983)
Co-authored-by: vkamn <vk@amnezia.org>
2025-11-12 15:57:53 +08:00
vkamn 3cc18c5807 chore: bump version (#1982) 2025-11-11 23:03:24 +08:00
NickVs2015 5fdce1e49e fix: fix ui android issues (#1980)
* Fix UI issues

* Fix Screen Swipe
2025-11-11 22:03:27 +08:00
Yaroslav 2ee61a040b fix: iOS appstore publish fix (#1922) 2025-11-04 12:10:30 +08:00
vkamn 741b5cc0f9 fix: qt6 9 support (#1973)
* Fix qt 6.9 support

* add support android sdk 36

* feat: add support SafeMargins from Android

* Fix black screen

---------

Co-authored-by: NickVs2015 <nv@amnezia.org>
2025-11-04 11:43:36 +08:00
MrMirDan aaf0e070dc fix: hide description (#1959) 2025-11-03 10:27:01 +08:00
vkamn e0e126eda8 chore: bump version (#1969) 2025-11-03 10:26:33 +08:00
vkamn 236daf6b3b feat: ad label (#1966)
* refactor: ad label desing refatroing

* feat: add ad label settings processing

* chore: fix ru translations

* chore: minor fixes
2025-11-03 10:26:22 +08:00
vkamn f1481b1b1f feat: add async post in gateway controller (#1963) 2025-10-29 23:24:24 +08:00
vkamn f6e7d3ccf1 fix: minor ui fixes (#1917)
* feat: improve storage processing

* fix: minor ui fixes
2025-10-09 23:22:58 +08:00
Mitternacht822 a754a11913 fix: added displaying vpn_key field added in older version of the app (#1873)
* fix(api_key): added displaying vpn_key field added in older version of the app

* revert changes

* fix: implemented generation of api key text for PremiumV2

* fix: deleted unnecessary code

* saving apikey text when generating

* added method for vpn key export, fixed wrong saving file
2025-10-07 23:16:28 +08:00
vkamn 4d25e3b6f6 chore: minor bugfixes (#1915) 2025-10-07 23:15:06 +08:00
MrMirDan 1fac280497 fix: main app info added after clearing logs (#1913) 2025-10-06 21:07:04 +08:00
Yaroslav c886c5e6a7 feat: enhance OpenVPN configuration handling and logging for iOS plat… (#1910)
* feat: enhance OpenVPN configuration handling and logging for iOS platform

* refactor: remove $OPENVPN_TA_KEY_SANITIZED and use $OPENVPN_TA_KEY instead
2025-10-06 21:04:49 +08:00
aiamnezia cd7f78b9ca feat: news and notifications page (#1660)
* Add news and notifications

* Add localization for news and notifications

* Remove news caching

* Add fetching news befor openning news page

* Fix not updating news page

* Delete debug output

* Remove news and notificztions with only self-hosted servers

* Add stack filters to fetching news request

* Add fetching news with changing stack in the client

* small refactoring

* polishing

* Rename newsModel files and fix naming in code

* fix: remove custom signals; fetch news only on stack expansion

* chore: delete unnecessary code

* chore: code style fixes

* fix: fixed memory leak in gateway controller

---------

Co-authored-by: vkamn <vk@amnezia.org>
2025-10-06 12:06:36 +08:00
vkamn a587d3230f fix: again fixed site link for features field (#1908) 2025-10-06 11:38:57 +08:00
MrMirDan 93e7b45136 fix: removed 'clear site list' button icon (#1909) 2025-10-06 11:37:42 +08:00
vkamn e024f71ce1 fix: allow remove expired api configs (#1907) 2025-10-03 14:45:12 +08:00
MrMirDan 50d1be7b4a chore: update for RU translation (#1893) 2025-10-02 20:59:45 +08:00
MrMirDan 3ec6d8973b fix: warning visible only on windows (#1900) 2025-10-02 20:59:23 +08:00
Yaroslav Gurov 3ea47d31a9 fix: restore dns after using xray (#1902) 2025-10-02 20:58:53 +08:00
vkamn 30c8cc4548 feat: add isConnectEvent field to api request (#1896) 2025-09-30 12:10:27 +08:00
vkamn 98586d2dd9 fix: fixed site link (#1897) 2025-09-30 12:07:27 +08:00
vkamn c66d8ecca0 chore: bump version (#1892) 2025-09-29 11:07:27 +08:00
vkamn db535f7e7d chore: increase default values (#1891) 2025-09-29 11:05:30 +08:00
vkamn 89f30d8c31 fix: fixed native wg obfuscation (#1890) 2025-09-29 10:58:44 +08:00
Yaroslav 8bce432824 fix: enable paste from clipboard on ios in addition to android (#1868) 2025-09-29 10:56:41 +08:00
MrMirDan f3539b2632 fix: proper wl name on connection key page (#1867)
* fix: proper wl name on connection key page

* some changings

* little change

* added missing import

* fix: proper wl default filename
2025-09-29 10:55:53 +08:00
MrMirDan 7a96c212f3 fix: rename user in search (#1847) 2025-09-29 10:51:52 +08:00
MrMirDan 2d5dc54e0f fix: keyboard navigation for text fields (#1879) 2025-09-29 10:50:57 +08:00
MrMirDan cef4c262e9 fix: keyboard fix for api 'connection key' buttons (#1872) 2025-09-29 10:50:18 +08:00
MrMirDan 34309261a8 fix: scrollbar always visible (#1877)
* fix: scrollbar always visible

* fix: scrollbar always visible on app split tunneling page
2025-09-29 10:49:19 +08:00
MrMirDan 657eeb40c7 fix: mirror error code link (#1863)
* fix: mirror error code link

* remake
2025-09-29 10:48:36 +08:00
MrMirDan b4938c2cc9 fix: default lang matching between app and OS (#1855)
* fix: default lang matching between app and OS

* remake

* fix: set default lang value
2025-09-29 10:47:54 +08:00
MrMirDan 524fefc5cb feat: warning on app split tunneling for windows (#1880) 2025-09-29 10:45:14 +08:00
Yaroslav 73f13404bb feat: add support for multiple scenes and handle URL contexts in iOS 13+ (#1889) 2025-09-29 10:40:58 +08:00
MrMirDan 5fc68cca83 fix: split tunneling restoration from backup (#1835) 2025-09-15 10:55:18 +08:00
Mitternacht822 fcb7b8fa8d fix: save/restore AmneziaDNS state (#1833) 2025-09-15 10:54:34 +08:00
aiamnezia a81e32ff95 fix: clean service/client logs in uninstall scripts (#1846)
- Windows (x64/x86):
  - Remove delegation to `AmneziaVPN.exe -c`
  - Delete `%ProgramData%\AmneziaVPN\log\AmneziaVPN-service.log`
  - Delete current user logs at `%AppData%\AmneziaVPN.ORG\AmneziaVPN\log`
  - Remove empty parent dirs (app/org, log)

- Linux:
  - Delete only `/var/log/AmneziaVPN/AmneziaVPN-service.log` (preserve `post-uninstall.log`)
  - Delete current user logs at `$HOME/.local/share/AmneziaVPN.ORG/AmneziaVPN/log`
2025-09-15 10:53:51 +08:00
albexk c897052107 chore: bump version (#1850) 2025-09-10 19:28:36 +08:00
vkamn 4d0efc7ea5 fix: remove duplicate m_vpnConnection delete from AmneziaApplication destructor (#1848) 2025-09-10 15:01:52 +08:00
Ivan a77842c9e3 feat: add server diagnostics script (#1837)
Co-authored-by: Ivan Istomin <istomin-ms@yandex.ru>
2025-09-09 19:33:35 +08:00
183 changed files with 10060 additions and 986 deletions
+75 -21
View File
@@ -44,6 +44,13 @@ jobs:
submodules: 'true'
fetch-depth: 10
- name: 'Get version from CMakeLists.txt'
id: get_version
run: |
VERSION=$(grep 'set(AMNEZIAVPN_VERSION' CMakeLists.txt | sed -E 's/.*AMNEZIAVPN_VERSION ([0-9]+.[0-9]+.[0-9]+.[0-9]+)\)/\1/')
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "Version: $VERSION"
- name: 'Setup ccache'
uses: hendrikmuhs/ccache-action@v1.2
@@ -55,13 +62,13 @@ jobs:
bash deploy/build_linux.sh
- name: 'Pack installer'
run: cd deploy && tar -cf AmneziaVPN_Linux_Installer.tar AmneziaVPN_Linux_Installer.bin
run: cd deploy && tar -cf AmneziaVPN_Linux_Installer.tar AmneziaVPN_Linux_Installer.bin && zip AmneziaVPN_${VERSION}_linux_x64.tar.zip AmneziaVPN_Linux_Installer.tar
- name: 'Upload installer artifact'
uses: actions/upload-artifact@v4
with:
name: AmneziaVPN_Linux_installer.tar
path: deploy/AmneziaVPN_Linux_Installer.tar
name: AmneziaVPN_${{ env.VERSION }}_linux_x64.tar.zip
path: deploy/AmneziaVPN_${{ env.VERSION }}_linux_x64.tar.zip
retention-days: 7
- name: 'Upload unpacked artifact'
@@ -102,6 +109,14 @@ jobs:
submodules: 'true'
fetch-depth: 10
- name: 'Get version from CMakeLists.txt'
id: get_version
shell: bash
run: |
VERSION=$(grep 'set(AMNEZIAVPN_VERSION' CMakeLists.txt | sed -E 's/.*AMNEZIAVPN_VERSION ([0-9]+.[0-9]+.[0-9]+.[0-9]+)\)/\1/')
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "Version: $VERSION"
- name: 'Setup ccache'
uses: hendrikmuhs/ccache-action@v1.2
@@ -132,11 +147,16 @@ jobs:
set QIF_BIN_DIR="${{ runner.temp }}\\Qt\\Tools\\QtInstallerFramework\\${{ env.QIF_VERSION }}\\bin"
call deploy\\build_windows.bat
- name: 'Rename Windows installer'
shell: cmd
run: |
copy AmneziaVPN_x${{ env.BUILD_ARCH }}.exe AmneziaVPN_%VERSION%_x64.exe
- name: 'Upload installer artifact'
uses: actions/upload-artifact@v4
with:
name: AmneziaVPN_Windows_installer
path: AmneziaVPN_x${{ env.BUILD_ARCH }}.exe
name: AmneziaVPN_${{ env.VERSION }}_x64.exe
path: AmneziaVPN_${{ env.VERSION }}_x64.exe
retention-days: 7
- name: 'Upload unpacked artifact'
@@ -331,7 +351,7 @@ jobs:
runs-on: macos-latest
env:
QT_VERSION: 6.8.0
QT_VERSION: 6.8.3
MAC_TEAM_ID: ${{ secrets.MAC_TEAM_ID }}
@@ -361,7 +381,7 @@ jobs:
xcode-version: '16.2.0'
- name: 'Install Qt'
uses: jurplel/install-qt-action@v3
uses: jurplel/install-qt-action@v4
with:
version: ${{ env.QT_VERSION }}
host: 'mac'
@@ -369,10 +389,15 @@ jobs:
arch: 'clang_64'
modules: 'qtremoteobjects qt5compat qtshadertools'
dir: ${{ runner.temp }}
#setup-python: 'true'
#set-env: 'true'
#extra: '--external 7z --base ${{ env.QT_MIRROR }}'
setup-python: 'true'
set-env: 'true'
extra: '--external 7z --base ${{ env.QT_MIRROR }}'
aqtversion: '==3.3.0'
py7zrversion: '==0.22.*'
extra: '--base ${{ env.QT_MIRROR }}'
cache: 'true'
- name: 'Get sources'
uses: actions/checkout@v4
@@ -380,6 +405,13 @@ jobs:
submodules: 'true'
fetch-depth: 10
- name: 'Get version from CMakeLists.txt'
id: get_version
run: |
VERSION=$(grep 'set(AMNEZIAVPN_VERSION' CMakeLists.txt | sed -E 's/.*AMNEZIAVPN_VERSION ([0-9]+.[0-9]+.[0-9]+.[0-9]+)\)/\1/')
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "Version: $VERSION"
- name: 'Setup ccache'
uses: hendrikmuhs/ccache-action@v1.2
@@ -388,11 +420,17 @@ jobs:
export QT_BIN_DIR="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/macos/bin"
bash deploy/build_macos.sh -n
- name: 'Pack macOS installer'
run: |
cd deploy/build/pkg
zip -r ../../AmneziaVPN_${VERSION}_macos.zip AmneziaVPN.pkg
cd ../../..
- name: 'Upload installer artifact'
uses: actions/upload-artifact@v4
with:
name: AmneziaVPN_MacOS_installer
path: deploy/build/pkg/AmneziaVPN.pkg
name: AmneziaVPN_${{ env.VERSION }}_macos.zip
path: deploy/AmneziaVPN_${{ env.VERSION }}_macos.zip
retention-days: 7
- name: 'Upload unpacked artifact'
@@ -469,8 +507,8 @@ jobs:
runs-on: ubuntu-latest
env:
ANDROID_BUILD_PLATFORM: android-34
QT_VERSION: 6.7.3
ANDROID_BUILD_PLATFORM: android-36
QT_VERSION: 6.8.3
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
@@ -551,6 +589,13 @@ jobs:
with:
submodules: 'true'
- name: 'Get version from CMakeLists.txt'
id: get_version
run: |
VERSION=$(grep 'set(AMNEZIAVPN_VERSION' CMakeLists.txt | sed -E 's/.*AMNEZIAVPN_VERSION ([0-9]+.[0-9]+.[0-9]+.[0-9]+)\)/\1/')
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "Version: $VERSION"
- name: 'Setup ccache'
uses: hendrikmuhs/ccache-action@v1.2
@@ -584,35 +629,44 @@ jobs:
shell: bash
run: ./deploy/build_android.sh --aab --apk all --build-platform ${{ env.ANDROID_BUILD_PLATFORM }}
- name: 'Rename Android APKs'
run: |
cd deploy/build
mv AmneziaVPN-x86_64-release.apk AmneziaVPN_${VERSION}_android9+_x86_64.apk
mv AmneziaVPN-x86-release.apk AmneziaVPN_${VERSION}_android9+_x86.apk
mv AmneziaVPN-arm64-v8a-release.apk AmneziaVPN_${VERSION}_android9+_arm64-v8a.apk
mv AmneziaVPN-armeabi-v7a-release.apk AmneziaVPN_${VERSION}_android9+_armeabi-v7a.apk
cd ../..
- name: 'Upload x86_64 apk'
uses: actions/upload-artifact@v4
with:
name: AmneziaVPN-android-x86_64
path: deploy/build/AmneziaVPN-x86_64-release.apk
name: AmneziaVPN_${{ env.VERSION }}_android9+_x86_64.apk
path: deploy/build/AmneziaVPN_${{ env.VERSION }}_android9+_x86_64.apk
compression-level: 0
retention-days: 7
- name: 'Upload x86 apk'
uses: actions/upload-artifact@v4
with:
name: AmneziaVPN-android-x86
path: deploy/build/AmneziaVPN-x86-release.apk
name: AmneziaVPN_${{ env.VERSION }}_android9+_x86.apk
path: deploy/build/AmneziaVPN_${{ env.VERSION }}_android9+_x86.apk
compression-level: 0
retention-days: 7
- name: 'Upload arm64-v8a apk'
uses: actions/upload-artifact@v4
with:
name: AmneziaVPN-android-arm64-v8a
path: deploy/build/AmneziaVPN-arm64-v8a-release.apk
name: AmneziaVPN_${{ env.VERSION }}_android9+_arm64-v8a.apk
path: deploy/build/AmneziaVPN_${{ env.VERSION }}_android9+_arm64-v8a.apk
compression-level: 0
retention-days: 7
- name: 'Upload armeabi-v7a apk'
uses: actions/upload-artifact@v4
with:
name: AmneziaVPN-android-armeabi-v7a
path: deploy/build/AmneziaVPN-armeabi-v7a-release.apk
name: AmneziaVPN_${{ env.VERSION }}_android9+_armeabi-v7a.apk
path: deploy/build/AmneziaVPN_${{ env.VERSION }}_android9+_armeabi-v7a.apk
compression-level: 0
retention-days: 7
+1
View File
@@ -9,6 +9,7 @@ deploy/build_32/*
deploy/build_64/*
winbuild*.bat
.cache/
.vscode/
# Qt-es
+2 -2
View File
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
set(PROJECT AmneziaVPN)
set(AMNEZIAVPN_VERSION 4.8.10.0)
set(AMNEZIAVPN_VERSION 4.8.11.5)
project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
DESCRIPTION "AmneziaVPN"
@@ -12,7 +12,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
set(RELEASE_DATE "${CURRENT_DATE}")
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
set(APP_ANDROID_VERSION_CODE 2093)
set(APP_ANDROID_VERSION_CODE 2100)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux")
+2 -1
View File
@@ -56,7 +56,7 @@ target_include_directories(${PROJECT} PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>
)
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_interface.rep)
qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_process_interface.rep)
qt_add_repc_replicas(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/../ipc/ipc_process_tun2socks.rep)
@@ -76,6 +76,7 @@ set(AMNEZIAVPN_TS_FILES
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_uk_UA.ts
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_ur_PK.ts
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_hi_IN.ts
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_es_ES.ts
)
file(GLOB_RECURSE AMNEZIAVPN_TS_SOURCES *.qrc *.cpp *.h *.ui)
+23 -3
View File
@@ -13,6 +13,8 @@
#include <QTimer>
#include <QTranslator>
#include <QEvent>
#include <QDir>
#include <QSettings>
#include "logger.h"
#include "ui/controllers/pageController.h"
@@ -54,15 +56,17 @@ AmneziaApplication::AmneziaApplication(int &argc, char *argv[]) : AMNEZIA_BASE_C
AmneziaApplication::~AmneziaApplication()
{
if (m_vpnConnection) {
QMetaObject::invokeMethod(m_vpnConnection.get(), "disconnectSlots", Qt::QueuedConnection);
QMetaObject::invokeMethod(m_vpnConnection.get(), "disconnectFromVpn", Qt::QueuedConnection);
QMetaObject::invokeMethod(m_vpnConnection.get(), "deleteLater", Qt::QueuedConnection);
QThread::msleep(2000);
}
m_vpnConnectionThread.requestInterruption();
m_vpnConnectionThread.quit();
if (!m_vpnConnectionThread.wait(5000)) {
if (!m_vpnConnectionThread.wait(3000)) {
m_vpnConnectionThread.terminate();
m_vpnConnectionThread.wait();
m_vpnConnectionThread.wait(500);
}
if (m_engine) {
@@ -71,8 +75,24 @@ AmneziaApplication::~AmneziaApplication()
}
}
#ifdef Q_OS_ANDROID
namespace {
static void clearQtCaches()
{
const QString cacheRoot = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
if (!cacheRoot.isEmpty()) {
QDir(cacheRoot + "/QtShaderCache").removeRecursively();
QDir(cacheRoot + "/qmlcache").removeRecursively();
}
}
}
#endif
void AmneziaApplication::init()
{
#ifdef Q_OS_ANDROID
clearQtCaches();
#endif
m_engine = new QQmlApplicationEngine;
const QUrl url(QStringLiteral("qrc:/ui/qml/main2.qml"));
+3 -2
View File
@@ -45,7 +45,8 @@
android:configChanges="uiMode|screenSize|smallestScreenSize|screenLayout|orientation|density
|fontScale|layoutDirection|locale|keyboard|keyboardHidden|navigation|mcc|mnc"
android:launchMode="singleInstance"
android:windowSoftInputMode="stateUnchanged|adjustResize"
android:windowSoftInputMode="adjustResize|stateUnchanged"
android:enableOnBackInvokedCallback="false"
android:exported="true">
<intent-filter>
@@ -214,4 +215,4 @@
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/qtprovider_paths" />
</provider>
</application>
</manifest>
</manifest>
+3
View File
@@ -6,6 +6,9 @@
<item name="android:colorBackground">@color/black</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:enforceNavigationBarContrast">false</item>
<item name="android:enforceStatusBarContrast">false</item>
</style>
<style name="Translucent" parent="NoActionBar">
<item name="android:windowBackground">@android:color/transparent</item>
@@ -35,6 +35,11 @@ import android.widget.Toast
import androidx.annotation.MainThread
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets
import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import java.io.IOException
import kotlin.LazyThreadSafetyMode.NONE
import kotlin.coroutines.CoroutineContext
@@ -170,10 +175,9 @@ class AmneziaActivity : QtActivity() {
super.onCreate(savedInstanceState)
Log.d(TAG, "Create Amnezia activity")
loadLibs()
window.apply {
addFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
statusBarColor = getColor(R.color.black)
}
// Configure window for edge-to-edge display
configureWindowForEdgeToEdge()
mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
val proto = mainScope.async(Dispatchers.IO) {
VpnStateStore.getVpnState().vpnProto
@@ -265,6 +269,82 @@ class AmneziaActivity : QtActivity() {
super.onStop()
}
override fun onResume() {
super.onResume()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
window.decorView.apply {
invalidate()
postDelayed({
sendTouch(1f, 1f)
}, 100)
postDelayed({
sendTouch(2f, 2f)
}, 200)
postDelayed({
requestLayout()
invalidate()
}, 250)
}
}
}
private fun configureWindowForEdgeToEdge() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
window.apply {
addFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
addFlags(LayoutParams.FLAG_LAYOUT_NO_LIMITS)
statusBarColor = android.graphics.Color.TRANSPARENT
navigationBarColor = android.graphics.Color.TRANSPARENT
}
WindowInsetsControllerCompat(window, window.decorView).apply {
isAppearanceLightStatusBars = false
isAppearanceLightNavigationBars = false
}
// Workaround for Android 14 (API 34+) IME adjustResize bug
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
setupImeInsetsListener()
}
} else {
window.apply {
addFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
statusBarColor = getColor(R.color.black)
}
}
}
private fun setupImeInsetsListener() {
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { view, windowInsets ->
val imeInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime())
val imeVisible = windowInsets.isVisible(WindowInsetsCompat.Type.ime())
val imeHeight = if (imeVisible) imeInsets.bottom else 0
val density = resources.displayMetrics.density
val imeHeightDp = (imeHeight / density).toInt()
// Also track system bars (navigation bar, status bar) changes
val systemBarsInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val navBarHeight = systemBarsInsets.bottom
val navBarHeightDp = (navBarHeight / density).toInt()
val statusBarHeight = systemBarsInsets.top
val statusBarHeightDp = (statusBarHeight / density).toInt()
mainScope.launch {
qtInitialized.await()
QtAndroidController.onImeInsetsChanged(imeHeightDp)
QtAndroidController.onSystemBarsInsetsChanged(navBarHeightDp, statusBarHeightDp)
}
// Return windowInsets instead of CONSUMED to allow proper handling
windowInsets
}
}
override fun onDestroy() {
Log.d(TAG, "Destroy Amnezia activity")
unregisterBroadcastReceiver(notificationStateReceiver)
@@ -666,6 +746,43 @@ class AmneziaActivity : QtActivity() {
@Suppress("unused")
fun isOnTv(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
@Suppress("unused")
fun isEdgeToEdgeEnabled(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
@Suppress("unused")
fun getStatusBarHeight(): Int {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) return 0
val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android")
val heightPx = if (resourceId > 0) {
resources.getDimensionPixelSize(resourceId)
} else {
0
}
// Convert physical pixels to device-independent pixels for QML
val density = resources.displayMetrics.density
val heightDp = (heightPx / density).toInt()
return heightDp
}
@Suppress("unused")
fun getNavigationBarHeight(): Int {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) return 0
val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")
val heightPx = if (resourceId > 0) {
resources.getDimensionPixelSize(resourceId)
} else {
0
}
// Convert physical pixels to device-independent pixels for QML
val density = resources.displayMetrics.density
val heightDp = (heightPx / density).toInt()
return heightDp
}
@Suppress("unused")
fun startQrCodeReader() {
Log.v(TAG, "Start camera")
@@ -38,15 +38,15 @@ object AppListProvider {
}
}
private class App(pi: PackageInfo, pm: PackageManager, ai: ApplicationInfo = pi.applicationInfo) : Comparable<App> {
private class App(pi: PackageInfo, pm: PackageManager, ai: ApplicationInfo? = pi.applicationInfo) : Comparable<App> {
val name: String?
val packageName: String = pi.packageName
val icon: Boolean = ai.icon != 0
val icon: Boolean = (ai?.icon ?: 0) != 0
val isLaunchable: Boolean = pm.getLaunchIntentForPackage(packageName) != null
init {
val name = ai.loadLabel(pm).toString()
this.name = if (name != packageName) name else null
val name = ai?.loadLabel(pm)?.toString()
this.name = name?.takeIf { it != packageName }
}
override fun compareTo(other: App): Int {
@@ -28,4 +28,7 @@ object QtAndroidController {
external fun onAuthResult(result: Boolean)
external fun decodeQrCode(data: String): Boolean
external fun onImeInsetsChanged(heightDp: Int)
external fun onSystemBarsInsetsChanged(navBarHeightDp: Int, statusBarHeightDp: Int)
}
+3 -3
View File
@@ -1,6 +1,6 @@
message("Client android ${CMAKE_ANDROID_ARCH_ABI} build")
set(APP_ANDROID_MIN_SDK 26)
set(APP_ANDROID_MIN_SDK 28)
set(ANDROID_PLATFORM "android-${APP_ANDROID_MIN_SDK}" CACHE STRING
"The minimum API level supported by the application or library" FORCE)
@@ -11,8 +11,8 @@ set_target_properties(${PROJECT} PROPERTIES
QT_ANDROID_VERSION_NAME ${CMAKE_PROJECT_VERSION}
QT_ANDROID_VERSION_CODE ${APP_ANDROID_VERSION_CODE}
QT_ANDROID_MIN_SDK_VERSION ${APP_ANDROID_MIN_SDK}
QT_ANDROID_TARGET_SDK_VERSION 34
QT_ANDROID_SDK_BUILD_TOOLS_REVISION 34.0.0
QT_ANDROID_TARGET_SDK_VERSION 36
QT_ANDROID_SDK_BUILD_TOOLS_REVISION 36.0.0
QT_ANDROID_PACKAGE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/android
)
+1
View File
@@ -46,6 +46,7 @@ set(SOURCES ${SOURCES}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/AmneziaSceneDelegateHooks.mm
)
+16 -3
View File
@@ -28,6 +28,7 @@ set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/../common/logger/logger.h
${CLIENT_ROOT_DIR}/utils/qmlUtils.h
${CLIENT_ROOT_DIR}/core/api/apiUtils.h
${CLIENT_ROOT_DIR}/core/osSignalHandler.h
)
# Mozilla headres
@@ -36,7 +37,6 @@ set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/mozilla/shared/ipaddress.h
${CLIENT_ROOT_DIR}/mozilla/shared/leakdetector.h
${CLIENT_ROOT_DIR}/mozilla/controllerimpl.h
${CLIENT_ROOT_DIR}/mozilla/localsocketcontroller.h
)
if(NOT IOS AND NOT MACOS_NE)
@@ -79,6 +79,7 @@ set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/../common/logger/logger.cpp
${CLIENT_ROOT_DIR}/utils/qmlUtils.cpp
${CLIENT_ROOT_DIR}/core/api/apiUtils.cpp
${CLIENT_ROOT_DIR}/core/osSignalHandler.cpp
)
# Mozilla sources
@@ -86,7 +87,6 @@ set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/mozilla/models/server.cpp
${CLIENT_ROOT_DIR}/mozilla/shared/ipaddress.cpp
${CLIENT_ROOT_DIR}/mozilla/shared/leakdetector.cpp
${CLIENT_ROOT_DIR}/mozilla/localsocketcontroller.cpp
)
if(NOT IOS AND NOT MACOS_NE)
@@ -175,7 +175,7 @@ if(WIN32)
)
endif()
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
if(WIN32 OR (APPLE AND NOT IOS AND NOT MACOS_NE) OR (LINUX AND NOT ANDROID))
message("Client desktop build")
add_compile_definitions(AMNEZIA_DESKTOP)
@@ -189,11 +189,13 @@ if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
${CLIENT_ROOT_DIR}/protocols/wireguardprotocol.h
${CLIENT_ROOT_DIR}/protocols/xrayprotocol.h
${CLIENT_ROOT_DIR}/protocols/awgprotocol.h
${CLIENT_ROOT_DIR}/mozilla/localsocketcontroller.h
)
set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/core/ipcclient.cpp
${CLIENT_ROOT_DIR}/core/privileged_process.cpp
${CLIENT_ROOT_DIR}/mozilla/localsocketcontroller.cpp
${CLIENT_ROOT_DIR}/ui/systemtray_notificationhandler.cpp
${CLIENT_ROOT_DIR}/protocols/openvpnprotocol.cpp
${CLIENT_ROOT_DIR}/protocols/openvpnovercloakprotocol.cpp
@@ -203,3 +205,14 @@ if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
${CLIENT_ROOT_DIR}/protocols/awgprotocol.cpp
)
endif()
if(APPLE AND MACOS_NE)
# Include only the tray notification handler in NE builds
set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/ui/systemtray_notificationhandler.h
)
set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/ui/systemtray_notificationhandler.cpp
)
endif()
+20 -2
View File
@@ -83,12 +83,30 @@ QString OpenVpnConfigurator::createConfig(const ServerCredentials &credentials,
return "";
}
auto sanitizeStaticKey = [](const QString &key) {
QStringList lines = key.split('\n');
QStringList filtered;
filtered.reserve(lines.size());
for (const QString &line : lines) {
const QString trimmed = line.trimmed();
if (trimmed.startsWith('#')) {
continue;
}
filtered.append(line);
}
QString result = filtered.join('\n');
if (!result.endsWith('\n')) {
result.append('\n');
}
return result;
};
config.replace("$OPENVPN_CA_CERT", connData.caCert);
config.replace("$OPENVPN_CLIENT_CERT", connData.clientCert);
config.replace("$OPENVPN_PRIV_KEY", connData.privKey);
if (config.contains("$OPENVPN_TA_KEY")) {
config.replace("$OPENVPN_TA_KEY", connData.taKey);
config.replace("$OPENVPN_TA_KEY", sanitizeStaticKey(connData.taKey));
} else {
config.replace("<tls-auth>", "");
config.replace("</tls-auth>", "");
@@ -117,7 +135,7 @@ QString OpenVpnConfigurator::processConfigWithLocalSettings(const QPair<QString,
if (!isApiConfig) {
QRegularExpression regex("redirect-gateway.*");
config.replace(regex, "");
// We don't use secondary DNS if primary DNS is AmneziaDNS
if (dns.first.contains(protocols::dns::amneziaDnsIp)) {
QRegularExpression dnsRegex("dhcp-option DNS " + dns.second);
+12
View File
@@ -47,12 +47,14 @@ namespace apiDefs
constexpr QLatin1String serverCountryName("server_country_name");
constexpr QLatin1String osVersion("os_version");
constexpr QLatin1String appLanguage("app_language");
constexpr QLatin1String availableCountries("available_countries");
constexpr QLatin1String activeDeviceCount("active_device_count");
constexpr QLatin1String maxDeviceCount("max_device_count");
constexpr QLatin1String subscriptionEndDate("subscription_end_date");
constexpr QLatin1String issuedConfigs("issued_configs");
constexpr QLatin1String subscriptionDescription("subscription_description");
constexpr QLatin1String supportInfo("support_info");
constexpr QLatin1String email("email");
@@ -64,6 +66,16 @@ namespace apiDefs
constexpr QLatin1String id("id");
constexpr QLatin1String orderId("order_id");
constexpr QLatin1String migrationCode("migration_code");
constexpr QLatin1String transactionId("transaction_id");
constexpr QLatin1String userCountryCode("user_country_code");
constexpr QLatin1String serviceInfo("service_info");
constexpr QLatin1String isAdVisible("is_ad_visible");
constexpr QLatin1String adHeader("ad_header");
constexpr QLatin1String adDescription("ad_description");
constexpr QLatin1String adEndpoint("ad_endpoint");
}
const int requestTimeoutMsecs = 12 * 1000; // 12 secs
+60 -12
View File
@@ -82,7 +82,9 @@ apiDefs::ConfigSource apiUtils::getConfigSource(const QJsonObject &serverConfigO
return static_cast<apiDefs::ConfigSource>(serverConfigObject.value(apiDefs::key::configVersion).toInt());
}
amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &sslErrors, QNetworkReply *reply)
amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &sslErrors, const QString &replyErrorString,
const QNetworkReply::NetworkError &replyError, const int httpStatusCode,
const QByteArray &responseBody)
{
const int httpStatusCodeConflict = 409;
const int httpStatusCodeNotFound = 404;
@@ -90,21 +92,19 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
if (!sslErrors.empty()) {
qDebug().noquote() << sslErrors;
return amnezia::ErrorCode::ApiConfigSslError;
} else if (reply->error() == QNetworkReply::NoError) {
} else if (replyError == QNetworkReply::NoError) {
return amnezia::ErrorCode::NoError;
} else if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError
|| reply->error() == QNetworkReply::NetworkError::TimeoutError) {
qDebug() << reply->error();
} else if (replyError == QNetworkReply::NetworkError::OperationCanceledError
|| replyError == QNetworkReply::NetworkError::TimeoutError) {
qDebug() << replyError;
return amnezia::ErrorCode::ApiConfigTimeoutError;
} else if (reply->error() == QNetworkReply::NetworkError::OperationNotImplementedError) {
qDebug() << reply->error();
} else if (replyError == QNetworkReply::NetworkError::OperationNotImplementedError) {
qDebug() << replyError;
return amnezia::ErrorCode::ApiUpdateRequestError;
} else {
QString err = reply->errorString();
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
qDebug() << QString::fromUtf8(reply->readAll());
qDebug() << reply->error();
qDebug() << err;
qDebug() << QString::fromUtf8(responseBody);
qDebug() << replyError;
qDebug() << replyErrorString;
qDebug() << httpStatusCode;
if (httpStatusCode == httpStatusCodeConflict) {
return amnezia::ErrorCode::ApiConfigLimitError;
@@ -162,3 +162,51 @@ QString apiUtils::getPremiumV1VpnKey(const QJsonObject &serverConfigObject)
return QString("vpn://%1").arg(QString(signedData.toBase64(QByteArray::Base64UrlEncoding)));
}
QString apiUtils::getPremiumV2VpnKey(const QJsonObject &serverConfigObject)
{
if (apiUtils::getConfigType(serverConfigObject) != apiDefs::ConfigType::AmneziaPremiumV2) {
return {};
}
QString vpnKeyText = "";
auto apiConfig = serverConfigObject.value(apiDefs::key::apiConfig).toObject();
auto authData = serverConfigObject.value(QLatin1String("auth_data")).toObject();
const QString name = serverConfigObject.value(apiDefs::key::name).toString();
const QString description = serverConfigObject.value(apiDefs::key::description).toString();
const double configVersion = serverConfigObject.value(apiDefs::key::configVersion).toDouble();
const QString serviceType = apiConfig.value(apiDefs::key::serviceType).toString();
const QString serviceProtocol = apiConfig.value(QLatin1String("service_protocol")).toString();
const QString userCountryCode = apiConfig.value(QLatin1String("user_country_code")).toString();
const QString apiKey = authData.value(apiDefs::key::apiKey).toString();
QString vpnKeyStr = "{";
vpnKeyStr += "\"" + QString(apiDefs::key::name) + "\": \"" + name + "\", ";
vpnKeyStr += "\"" + QString(apiDefs::key::description) + "\": \"" + description + "\", ";
vpnKeyStr += "\"" + QString(apiDefs::key::configVersion) + "\": " + QString::number(static_cast<int>(configVersion)) + ", ";
vpnKeyStr += "\"" + QString(apiDefs::key::apiConfig) + "\": {";
vpnKeyStr += "\"" + QString(apiDefs::key::serviceType) + "\": \"" + serviceType + "\", ";
vpnKeyStr += "\"service_protocol\": \"" + serviceProtocol + "\", ";
vpnKeyStr += "\"user_country_code\": \"" + userCountryCode + "\"";
vpnKeyStr += "}, ";
vpnKeyStr += "\"auth_data\": {";
vpnKeyStr += "\"" + QString(apiDefs::key::apiKey) + "\": \"" + apiKey + "\"";
vpnKeyStr += "}";
vpnKeyStr += "}";
QByteArray vpnKeyCompressed = escapeUnicode(vpnKeyStr).toUtf8();
vpnKeyCompressed = qCompress(vpnKeyCompressed, 6);
vpnKeyCompressed = vpnKeyCompressed.mid(4);
QByteArray signedData = AMNEZIA_CONFIG_SIGNATURE + vpnKeyCompressed;
vpnKeyText = QString("vpn://%1").arg(QString(signedData.toBase64(QByteArray::Base64UrlEncoding)));
return vpnKeyText;
}
+4 -1
View File
@@ -18,9 +18,12 @@ namespace apiUtils
apiDefs::ConfigType getConfigType(const QJsonObject &serverConfigObject);
apiDefs::ConfigSource getConfigSource(const QJsonObject &serverConfigObject);
amnezia::ErrorCode checkNetworkReplyErrors(const QList<QSslError> &sslErrors, QNetworkReply *reply);
amnezia::ErrorCode checkNetworkReplyErrors(const QList<QSslError> &sslErrors, const QString &replyErrorString,
const QNetworkReply::NetworkError &replyError, const int httpStatusCode,
const QByteArray &responseBody);
QString getPremiumV1VpnKey(const QJsonObject &serverConfigObject);
QString getPremiumV2VpnKey(const QJsonObject &serverConfigObject);
}
#endif // APIUTILS_H
+12 -2
View File
@@ -26,9 +26,8 @@ CoreController::CoreController(const QSharedPointer<VpnConnection> &vpnConnectio
initNotificationHandler();
auto locale = m_settings->getAppLanguage();
m_translator.reset(new QTranslator());
updateTranslator(locale);
updateTranslator(m_settings->getAppLanguage());
}
void CoreController::initModels()
@@ -100,6 +99,9 @@ void CoreController::initModels()
m_apiDevicesModel.reset(new ApiDevicesModel(m_settings, this));
m_engine->rootContext()->setContextProperty("ApiDevicesModel", m_apiDevicesModel.get());
m_newsModel.reset(new NewsModel(m_settings, this));
m_engine->rootContext()->setContextProperty("NewsModel", m_newsModel.get());
}
void CoreController::initControllers()
@@ -154,6 +156,9 @@ void CoreController::initControllers()
m_apiPremV1MigrationController.reset(new ApiPremV1MigrationController(m_serversModel, m_settings, this));
m_engine->rootContext()->setContextProperty("ApiPremV1MigrationController", m_apiPremV1MigrationController.get());
m_apiNewsController.reset(new ApiNewsController(m_newsModel, m_settings, m_serversModel, this));
m_engine->rootContext()->setContextProperty("ApiNewsController", m_apiNewsController.get());
}
void CoreController::initAndroidController()
@@ -317,6 +322,11 @@ void CoreController::initContainerModelUpdateHandler()
connect(m_serversModel.get(), &ServersModel::containersUpdated, m_containersModel.get(), &ContainersModel::updateModel);
connect(m_serversModel.get(), &ServersModel::defaultServerContainersUpdated, m_defaultServerContainersModel.get(),
&ContainersModel::updateModel);
connect(m_serversModel.get(), &ServersModel::gatewayStacksExpanded, this, [this]() {
if (m_serversModel->hasServersFromGatewayApi()) {
m_apiNewsController->fetchNews(false);
}
});
m_serversModel->resetModel();
}
+4
View File
@@ -12,6 +12,7 @@
#include "ui/controllers/api/apiConfigsController.h"
#include "ui/controllers/api/apiSettingsController.h"
#include "ui/controllers/api/apiPremV1MigrationController.h"
#include "ui/controllers/api/apiNewsController.h"
#include "ui/controllers/appSplitTunnelingController.h"
#include "ui/controllers/allowedDnsController.h"
#include "ui/controllers/connectionController.h"
@@ -47,6 +48,7 @@
#include "ui/models/services/sftpConfigModel.h"
#include "ui/models/services/socks5ProxyConfigModel.h"
#include "ui/models/sites_model.h"
#include "ui/models/newsModel.h"
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
#include "ui/notificationhandler.h"
@@ -118,6 +120,7 @@ private:
QScopedPointer<ApiSettingsController> m_apiSettingsController;
QScopedPointer<ApiConfigsController> m_apiConfigsController;
QScopedPointer<ApiPremV1MigrationController> m_apiPremV1MigrationController;
QScopedPointer<ApiNewsController> m_apiNewsController;
QSharedPointer<ContainersModel> m_containersModel;
QSharedPointer<ContainersModel> m_defaultServerContainersModel;
@@ -125,6 +128,7 @@ private:
QSharedPointer<LanguageModel> m_languageModel;
QSharedPointer<ProtocolsModel> m_protocolsModel;
QSharedPointer<SitesModel> m_sitesModel;
QSharedPointer<NewsModel> m_newsModel;
QSharedPointer<AllowedDnsModel> m_allowedDnsModel;
QSharedPointer<AppSplitTunnelingModel> m_appSplitTunnelingModel;
QSharedPointer<ClientManagementModel> m_clientManagementModel;
+322 -125
View File
@@ -1,12 +1,15 @@
#include "gatewayController.h"
#include <algorithm>
#include <functional>
#include <random>
#include <QCryptographicHash>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include <QPromise>
#include <QUrl>
#include "QBlockCipher.h"
@@ -50,87 +53,25 @@ GatewayController::GatewayController(const QString &gatewayEndpoint, const bool
{
}
ErrorCode GatewayController::get(const QString &endpoint, QByteArray &responseBody)
GatewayController::EncryptedRequestData GatewayController::prepareRequest(const QString &endpoint, const QJsonObject &apiPayload)
{
EncryptedRequestData encRequestData;
encRequestData.errorCode = ErrorCode::NoError;
#ifdef Q_OS_IOS
IosController::Instance()->requestInetAccess();
QThread::msleep(10);
#endif
QNetworkRequest request;
request.setTransferTimeout(m_requestTimeoutMsecs);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader(QString("X-Client-Request-ID").toUtf8(), QUuid::createUuid().toString(QUuid::WithoutBraces).toUtf8());
request.setUrl(QString(endpoint).arg(m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl));
encRequestData.request.setTransferTimeout(m_requestTimeoutMsecs);
encRequestData.request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
encRequestData.request.setRawHeader(QString("X-Client-Request-ID").toUtf8(), QUuid::createUuid().toString(QUuid::WithoutBraces).toUtf8());
encRequestData.request.setUrl(endpoint.arg(m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl));
// bypass killSwitch exceptions for API-gateway
#ifdef AMNEZIA_DESKTOP
if (m_isStrictKillSwitchEnabled) {
QString host = QUrl(request.url()).host();
QString ip = NetworkUtilities::getIPAddress(host);
if (!ip.isEmpty()) {
IpcClient::Interface()->addKillSwitchAllowedRange(QStringList { ip });
}
}
#endif
QNetworkReply *reply;
reply = amnApp->networkManager()->get(request);
QEventLoop wait;
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
QList<QSslError> sslErrors;
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec();
responseBody = reply->readAll();
if (sslErrors.isEmpty() && shouldBypassProxy(reply, responseBody, false)) {
auto requestFunction = [&request, &responseBody](const QString &url) {
request.setUrl(url);
return amnApp->networkManager()->get(request);
};
auto replyProcessingFunction = [&responseBody, &reply, &sslErrors, this](QNetworkReply *nestedReply,
const QList<QSslError> &nestedSslErrors) {
responseBody = nestedReply->readAll();
if (!sslErrors.isEmpty() || !shouldBypassProxy(nestedReply, responseBody, false)) {
sslErrors = nestedSslErrors;
reply = nestedReply;
return true;
}
return false;
};
bypassProxy(endpoint, reply, requestFunction, replyProcessingFunction);
}
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, reply);
reply->deleteLater();
return errorCode;
}
ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
{
#ifdef Q_OS_IOS
IosController::Instance()->requestInetAccess();
QThread::msleep(10);
#endif
QNetworkRequest request;
request.setTransferTimeout(m_requestTimeoutMsecs);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader(QString("X-Client-Request-ID").toUtf8(), QUuid::createUuid().toString(QUuid::WithoutBraces).toUtf8());
request.setUrl(endpoint.arg(m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl));
// bypass killSwitch exceptions for API-gateway
#ifdef AMNEZIA_DESKTOP
if (m_isStrictKillSwitchEnabled) {
QString host = QUrl(request.url()).host();
QString host = QUrl(encRequestData.request.url()).host();
QString ip = NetworkUtilities::getIPAddress(host);
if (!ip.isEmpty()) {
IpcClient::Interface()->addKillSwitchAllowedRange(QStringList { ip });
@@ -139,14 +80,14 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
#endif
QSimpleCrypto::QBlockCipher blockCipher;
QByteArray key = blockCipher.generatePrivateSalt(32);
QByteArray iv = blockCipher.generatePrivateSalt(32);
QByteArray salt = blockCipher.generatePrivateSalt(8);
encRequestData.key = blockCipher.generatePrivateSalt(32);
encRequestData.iv = blockCipher.generatePrivateSalt(32);
encRequestData.salt = blockCipher.generatePrivateSalt(8);
QJsonObject keyPayload;
keyPayload[configKey::aesKey] = QString(key.toBase64());
keyPayload[configKey::aesIv] = QString(iv.toBase64());
keyPayload[configKey::aesSalt] = QString(salt.toBase64());
keyPayload[configKey::aesKey] = QString(encRequestData.key.toBase64());
keyPayload[configKey::aesIv] = QString(encRequestData.iv.toBase64());
keyPayload[configKey::aesSalt] = QString(encRequestData.salt.toBase64());
QByteArray encryptedKeyPayload;
QByteArray encryptedApiPayload;
@@ -161,62 +102,88 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
} catch (...) {
Utils::logException();
qCritical() << "error loading public key from environment variables";
return ErrorCode::ApiMissingAgwPublicKey;
encRequestData.errorCode = ErrorCode::ApiMissingAgwPublicKey;
return encRequestData;
}
encryptedKeyPayload = rsa.encrypt(QJsonDocument(keyPayload).toJson(), publicKey, RSA_PKCS1_PADDING);
EVP_PKEY_free(publicKey);
encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(), key, iv, "", salt);
} catch (...) { // todo change error handling in QSimpleCrypto?
encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(), encRequestData.key, encRequestData.iv,
"", encRequestData.salt);
} catch (...) {
Utils::logException();
qCritical() << "error when encrypting the request body";
return ErrorCode::ApiConfigDecryptionError;
encRequestData.errorCode = ErrorCode::ApiConfigDecryptionError;
return encRequestData;
}
QJsonObject requestBody;
requestBody[configKey::keyPayload] = QString(encryptedKeyPayload.toBase64());
requestBody[configKey::apiPayload] = QString(encryptedApiPayload.toBase64());
QNetworkReply *reply = amnApp->networkManager()->post(request, QJsonDocument(requestBody).toJson());
encRequestData.requestBody = QJsonDocument(requestBody).toJson();
return encRequestData;
}
ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
{
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload);
if (encRequestData.errorCode != ErrorCode::NoError) {
return encRequestData.errorCode;
}
QNetworkReply *reply = amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
QEventLoop wait;
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
QList<QSslError> sslErrors;
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec();
wait.exec(QEventLoop::ExcludeUserInputEvents);
QByteArray encryptedResponseBody = reply->readAll();
QString replyErrorString = reply->errorString();
auto replyError = reply->error();
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (sslErrors.isEmpty() && shouldBypassProxy(reply, encryptedResponseBody, true, key, iv, salt)) {
auto requestFunction = [&request, &encryptedResponseBody, &requestBody](const QString &url) {
request.setUrl(url);
return amnApp->networkManager()->post(request, QJsonDocument(requestBody).toJson());
reply->deleteLater();
if (sslErrors.isEmpty()
&& shouldBypassProxy(replyError, encryptedResponseBody, true, encRequestData.key, encRequestData.iv, encRequestData.salt)) {
auto requestFunction = [&encRequestData, &encryptedResponseBody](const QString &url) {
encRequestData.request.setUrl(url);
return amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
};
auto replyProcessingFunction = [&encryptedResponseBody, &reply, &sslErrors, &key, &iv, &salt,
this](QNetworkReply *nestedReply, const QList<QSslError> &nestedSslErrors) {
encryptedResponseBody = nestedReply->readAll();
reply = nestedReply;
if (!sslErrors.isEmpty() || shouldBypassProxy(nestedReply, encryptedResponseBody, true, key, iv, salt)) {
auto replyProcessingFunction = [&encryptedResponseBody, &replyErrorString, &replyError, &httpStatusCode, &sslErrors,
&encRequestData, this](QNetworkReply *reply, const QList<QSslError> &nestedSslErrors) {
encryptedResponseBody = reply->readAll();
replyErrorString = reply->errorString();
replyError = reply->error();
httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (!sslErrors.isEmpty()
|| shouldBypassProxy(replyError, encryptedResponseBody, true, encRequestData.key, encRequestData.iv, encRequestData.salt)) {
sslErrors = nestedSslErrors;
return false;
}
return true;
};
bypassProxy(endpoint, reply, requestFunction, replyProcessingFunction);
auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString("");
bypassProxy(endpoint, serviceType, userCountryCode, requestFunction, replyProcessingFunction);
}
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, reply);
reply->deleteLater();
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, encryptedResponseBody);
if (errorCode) {
return errorCode;
}
try {
responseBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt);
QSimpleCrypto::QBlockCipher blockCipher;
responseBody =
blockCipher.decryptAesBlockCipher(encryptedResponseBody, encRequestData.key, encRequestData.iv, "", encRequestData.salt);
return ErrorCode::NoError;
} catch (...) { // todo change error handling in QSimpleCrypto?
Utils::logException();
@@ -225,7 +192,94 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
}
}
QStringList GatewayController::getProxyUrls()
QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload)
{
auto promise = QSharedPointer<QPromise<QPair<ErrorCode, QByteArray>>>::create();
promise->start();
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload);
if (encRequestData.errorCode != ErrorCode::NoError) {
promise->addResult(qMakePair(encRequestData.errorCode, QByteArray()));
promise->finish();
return promise->future();
}
QNetworkReply *reply = amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
auto sslErrors = QSharedPointer<QList<QSslError>>::create();
connect(reply, &QNetworkReply::sslErrors, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
connect(reply, &QNetworkReply::finished, reply, [promise, sslErrors, encRequestData, endpoint, apiPayload, reply, this]() mutable {
QByteArray encryptedResponseBody = reply->readAll();
QString replyErrorString = reply->errorString();
auto replyError = reply->error();
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
reply->deleteLater();
auto processResponse = [promise, encRequestData](const QByteArray &ecryptedResponseBody, const QList<QSslError> &sslErrors,
QNetworkReply::NetworkError replyError, const QString &replyErrorString,
int httpStatusCode) {
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, ecryptedResponseBody);
if (errorCode) {
promise->addResult(qMakePair(errorCode, QByteArray()));
promise->finish();
return;
}
QSimpleCrypto::QBlockCipher blockCipher;
try {
QByteArray responseBody = blockCipher.decryptAesBlockCipher(ecryptedResponseBody, encRequestData.key, encRequestData.iv, "",
encRequestData.salt);
promise->addResult(qMakePair(ErrorCode::NoError, responseBody));
promise->finish();
} catch (...) {
Utils::logException();
qCritical() << "error when decrypting the request body";
promise->addResult(qMakePair(ErrorCode::ApiConfigDecryptionError, QByteArray()));
promise->finish();
}
};
if (sslErrors->isEmpty()
&& shouldBypassProxy(replyError, encryptedResponseBody, true, encRequestData.key, encRequestData.iv, encRequestData.salt)) {
auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString("");
QStringList baseUrls;
if (m_isDevEnvironment) {
baseUrls = QString(DEV_S3_ENDPOINT).split(", ");
} else {
baseUrls = QString(PROD_S3_ENDPOINT).split(", ");
}
QStringList proxyStorageUrls;
if (!serviceType.isEmpty()) {
for (const auto &baseUrl : baseUrls) {
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
proxyStorageUrls.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)
+ ".json");
}
}
for (const auto &baseUrl : baseUrls)
proxyStorageUrls.push_back(baseUrl + "endpoints.json");
getProxyUrlsAsync(proxyStorageUrls, 0, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
getProxyUrlAsync(proxyUrls, 0, [this, encRequestData, endpoint, processResponse](const QString &proxyUrls) {
bypassProxyAsync(endpoint, proxyUrls, encRequestData, processResponse);
});
});
} else {
processResponse(encryptedResponseBody, *sslErrors, replyError, replyErrorString, httpStatusCode);
}
});
return promise->future();
}
QStringList GatewayController::getProxyUrls(const QString &serviceType, const QString &userCountryCode)
{
QNetworkRequest request;
request.setTransferTimeout(m_requestTimeoutMsecs);
@@ -235,22 +289,33 @@ QStringList GatewayController::getProxyUrls()
QList<QSslError> sslErrors;
QNetworkReply *reply;
QStringList proxyStorageUrls;
QStringList baseUrls;
if (m_isDevEnvironment) {
proxyStorageUrls = QString(DEV_S3_ENDPOINT).split(", ");
baseUrls = QString(DEV_S3_ENDPOINT).split(", ");
} else {
proxyStorageUrls = QString(PROD_S3_ENDPOINT).split(", ");
baseUrls = QString(PROD_S3_ENDPOINT).split(", ");
}
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
QStringList proxyStorageUrls;
if (!serviceType.isEmpty()) {
for (const auto &baseUrl : baseUrls) {
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
proxyStorageUrls.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json");
}
}
for (const auto &baseUrl : baseUrls) {
proxyStorageUrls.push_back(baseUrl + "endpoints.json");
}
for (const auto &proxyStorageUrl : proxyStorageUrls) {
request.setUrl(proxyStorageUrl);
reply = amnApp->networkManager()->get(request);
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec();
wait.exec(QEventLoop::ExcludeUserInputEvents);
if (reply->error() == QNetworkReply::NetworkError::NoError) {
auto encryptedResponseBody = reply->readAll();
@@ -288,7 +353,10 @@ QStringList GatewayController::getProxyUrls()
}
return endpoints;
} else {
apiUtils::checkNetworkReplyErrors(sslErrors, reply);
auto replyError = reply->error();
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
qDebug() << replyError;
qDebug() << httpStatusCode;
qDebug() << "go to the next storage endpoint";
reply->deleteLater();
@@ -297,33 +365,33 @@ QStringList GatewayController::getProxyUrls()
return {};
}
bool GatewayController::shouldBypassProxy(QNetworkReply *reply, const QByteArray &responseBody, bool checkEncryption, const QByteArray &key,
const QByteArray &iv, const QByteArray &salt)
bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &responseBody,
bool checkEncryption, const QByteArray &key, const QByteArray &iv, const QByteArray &salt)
{
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError || reply->error() == QNetworkReply::NetworkError::TimeoutError) {
if (replyError == QNetworkReply::NetworkError::OperationCanceledError || replyError == QNetworkReply::NetworkError::TimeoutError) {
qDebug() << "timeout occurred";
qDebug() << reply->error();
qDebug() << replyError;
return true;
} else if (responseBody.contains("html")) {
qDebug() << "the response contains an html tag";
return true;
} else if (reply->error() == QNetworkReply::NetworkError::ContentNotFoundError) {
} else if (replyError == QNetworkReply::NetworkError::ContentNotFoundError) {
if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2)
|| responseBody.contains(errorResponsePattern3)) {
return false;
} else {
qDebug() << reply->error();
qDebug() << replyError;
return true;
}
} else if (reply->error() == QNetworkReply::NetworkError::OperationNotImplementedError) {
} else if (replyError == QNetworkReply::NetworkError::OperationNotImplementedError) {
if (responseBody.contains(updateRequestResponsePattern)) {
return false;
} else {
qDebug() << reply->error();
qDebug() << replyError;
return true;
}
} else if (reply->error() != QNetworkReply::NetworkError::NoError) {
qDebug() << reply->error();
} else if (replyError != QNetworkReply::NetworkError::NoError) {
qDebug() << replyError;
return true;
} else if (checkEncryption) {
try {
@@ -337,35 +405,33 @@ bool GatewayController::shouldBypassProxy(QNetworkReply *reply, const QByteArray
return false;
}
void GatewayController::bypassProxy(const QString &endpoint, QNetworkReply *reply,
void GatewayController::bypassProxy(const QString &endpoint, const QString &serviceType, const QString &userCountryCode,
std::function<QNetworkReply *(const QString &url)> requestFunction,
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction)
{
QStringList proxyUrls = getProxyUrls();
QStringList proxyUrls = getProxyUrls(serviceType, userCountryCode);
std::random_device randomDevice;
std::mt19937 generator(randomDevice());
std::shuffle(proxyUrls.begin(), proxyUrls.end(), generator);
QByteArray responseBody;
auto bypassFunction = [this](const QString &endpoint, const QString &proxyUrl, QNetworkReply *reply,
auto bypassFunction = [this](const QString &endpoint, const QString &proxyUrl,
std::function<QNetworkReply *(const QString &url)> requestFunction,
std::function<bool(QNetworkReply * reply, const QList<QSslError> &sslErrors)> replyProcessingFunction) {
QEventLoop wait;
QList<QSslError> sslErrors;
qDebug() << "go to the next proxy endpoint";
reply->deleteLater(); // delete the previous reply
reply = requestFunction(endpoint.arg(proxyUrl));
QNetworkReply *reply = requestFunction(endpoint.arg(proxyUrl));
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec();
wait.exec(QEventLoop::ExcludeUserInputEvents);
if (replyProcessingFunction(reply, sslErrors)) {
return true;
}
return false;
auto result = replyProcessingFunction(reply, sslErrors);
reply->deleteLater();
return result;
};
if (m_proxyUrl.isEmpty()) {
@@ -383,7 +449,7 @@ void GatewayController::bypassProxy(const QString &endpoint, QNetworkReply *repl
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec();
wait.exec(QEventLoop::ExcludeUserInputEvents);
if (reply->error() == QNetworkReply::NetworkError::NoError) {
reply->deleteLater();
@@ -399,15 +465,146 @@ void GatewayController::bypassProxy(const QString &endpoint, QNetworkReply *repl
}
if (!m_proxyUrl.isEmpty()) {
if (bypassFunction(endpoint, m_proxyUrl, reply, requestFunction, replyProcessingFunction)) {
if (bypassFunction(endpoint, m_proxyUrl, requestFunction, replyProcessingFunction)) {
return;
}
}
for (const QString &proxyUrl : proxyUrls) {
if (bypassFunction(endpoint, proxyUrl, reply, requestFunction, replyProcessingFunction)) {
if (bypassFunction(endpoint, proxyUrl, requestFunction, replyProcessingFunction)) {
m_proxyUrl = proxyUrl;
break;
}
}
}
void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
std::function<void(const QStringList &)> onComplete)
{
if (currentProxyStorageIndex >= proxyStorageUrls.size()) {
onComplete({});
return;
}
QNetworkRequest request;
request.setTransferTimeout(m_requestTimeoutMsecs);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setUrl(proxyStorageUrls[currentProxyStorageIndex]);
QNetworkReply *reply = amnApp->networkManager()->get(request);
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) { *(state->sslErrors) = e; });
connect(reply, &QNetworkReply::finished, this, [this, proxyStorageUrls, currentProxyStorageIndex, onComplete, reply]() {
if (reply->error() == QNetworkReply::NoError) {
QByteArray encrypted = reply->readAll();
reply->deleteLater();
QByteArray responseBody;
try {
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
if (!m_isDevEnvironment) {
QCryptographicHash hash(QCryptographicHash::Sha512);
hash.addData(key);
QByteArray h = hash.result().toHex();
QByteArray decKey = QByteArray::fromHex(h.left(64));
QByteArray iv = QByteArray::fromHex(h.mid(64, 32));
QByteArray ba = QByteArray::fromBase64(encrypted);
QSimpleCrypto::QBlockCipher cipher;
responseBody = cipher.decryptAesBlockCipher(ba, decKey, iv);
} else {
responseBody = encrypted;
}
} catch (...) {
Utils::logException();
qCritical() << "error decrypting payload";
QMetaObject::invokeMethod(
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
return;
}
QJsonArray endpointsArray = QJsonDocument::fromJson(responseBody).array();
QStringList endpoints;
for (const QJsonValue &endpoint : endpointsArray)
endpoints.push_back(endpoint.toString());
QStringList shuffled = endpoints;
std::random_device randomDevice;
std::mt19937 generator(randomDevice());
std::shuffle(shuffled.begin(), shuffled.end(), generator);
onComplete(shuffled);
return;
}
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
qDebug() << httpStatusCode;
qDebug() << "go to the next storage endpoint";
reply->deleteLater();
QMetaObject::invokeMethod(
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
});
}
void GatewayController::getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function<void(const QString &)> onComplete)
{
if (currentProxyIndex >= proxyUrls.size()) {
onComplete("");
return;
}
QNetworkRequest request;
request.setTransferTimeout(1000);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setUrl(proxyUrls[currentProxyIndex] + "lmbd-health");
QNetworkReply *reply = amnApp->networkManager()->get(request);
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) {
// *(state->sslErrors) = e;
// });
connect(reply, &QNetworkReply::finished, this, [this, proxyUrls, currentProxyIndex, onComplete, reply]() {
reply->deleteLater();
if (reply->error() == QNetworkReply::NoError) {
m_proxyUrl = proxyUrls[currentProxyIndex];
onComplete(m_proxyUrl);
return;
}
qDebug() << "go to the next proxy endpoint";
QMetaObject::invokeMethod(this, [=]() { getProxyUrlAsync(proxyUrls, currentProxyIndex + 1, onComplete); }, Qt::QueuedConnection);
});
}
void GatewayController::bypassProxyAsync(
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
std::function<void(const QByteArray &, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete)
{
auto sslErrors = QSharedPointer<QList<QSslError>>::create();
if (proxyUrl.isEmpty()) {
onComplete(QByteArray(), *sslErrors, QNetworkReply::InternalServerError, "empty proxy url", 0);
return;
}
QNetworkRequest request = encRequestData.request;
request.setUrl(endpoint.arg(proxyUrl));
QNetworkReply *reply = amnApp->networkManager()->post(request, encRequestData.requestBody);
connect(reply, &QNetworkReply::sslErrors, this, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
connect(reply, &QNetworkReply::finished, this, [sslErrors, onComplete, reply]() {
QByteArray encryptedResponseBody = reply->readAll();
QString replyErrorString = reply->errorString();
auto replyError = reply->error();
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
reply->deleteLater();
onComplete(encryptedResponseBody, *sslErrors, replyError, replyErrorString, httpStatusCode);
});
}
+29 -5
View File
@@ -1,8 +1,12 @@
#ifndef GATEWAYCONTROLLER_H
#define GATEWAYCONTROLLER_H
#include <QFuture>
#include <QNetworkReply>
#include <QObject>
#include <QPair>
#include <QPromise>
#include <QSharedPointer>
#include "core/defs.h"
@@ -18,16 +22,36 @@ public:
explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
const bool isStrictKillSwitchEnabled, QObject *parent = nullptr);
amnezia::ErrorCode get(const QString &endpoint, QByteArray &responseBody);
amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody);
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject apiPayload);
private:
QStringList getProxyUrls();
bool shouldBypassProxy(QNetworkReply *reply, const QByteArray &responseBody, bool checkEncryption, const QByteArray &key = "",
const QByteArray &iv = "", const QByteArray &salt = "");
void bypassProxy(const QString &endpoint, QNetworkReply *reply, std::function<QNetworkReply *(const QString &url)> requestFunction,
struct EncryptedRequestData
{
QNetworkRequest request;
QByteArray requestBody;
QByteArray key;
QByteArray iv;
QByteArray salt;
amnezia::ErrorCode errorCode;
};
EncryptedRequestData prepareRequest(const QString &endpoint, const QJsonObject &apiPayload);
QStringList getProxyUrls(const QString &serviceType, const QString &userCountryCode);
bool shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &responseBody, bool checkEncryption,
const QByteArray &key = "", const QByteArray &iv = "", const QByteArray &salt = "");
void bypassProxy(const QString &endpoint, const QString &serviceType, const QString &userCountryCode,
std::function<QNetworkReply *(const QString &url)> requestFunction,
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction);
void getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
std::function<void(const QStringList &)> onComplete);
void getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function<void(const QString &)> onComplete);
void bypassProxyAsync(
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
std::function<void(const QByteArray &, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete);
int m_requestTimeoutMsecs;
QString m_gatewayEndpoint;
bool m_isDevEnvironment = false;
+22 -1
View File
@@ -18,6 +18,22 @@ bool IpcClient::isSocketConnected() const
return m_isSocketConnected;
}
void IpcClient::closeAndResetInstance(bool deleteSelf)
{
if (m_localSocket)
{
m_localSocket->disconnectFromServer();
m_localSocket->deleteLater();
m_localSocket.clear();
}
m_ipcClient.reset();
m_Tun2SocksClient.reset();
m_isSocketConnected = false;
if (deleteSelf) {
m_instance = nullptr;
}
}
IpcClient *IpcClient::Instance()
{
return m_instance;
@@ -39,6 +55,10 @@ QSharedPointer<IpcProcessTun2SocksReplica> IpcClient::InterfaceTun2Socks()
bool IpcClient::init(IpcClient *instance)
{
if (m_instance && m_instance != instance) {
m_instance->closeAndResetInstance(false);
m_instance->deleteLater();
}
m_instance = instance;
Instance()->m_localSocket = new QLocalSocket(Instance());
@@ -85,8 +105,9 @@ bool IpcClient::init(IpcClient *instance)
}
qDebug() << "IpcClient::init succeed";
instance->m_isSocketConnected = (Instance()->m_ipcClient->isReplicaValid() && Instance()->m_Tun2SocksClient->isReplicaValid());
return (Instance()->m_ipcClient->isReplicaValid() && Instance()->m_Tun2SocksClient->isReplicaValid());
return Instance()->isSocketConnected();
}
QSharedPointer<PrivilegedProcess> IpcClient::CreatePrivilegedProcess()
+1
View File
@@ -23,6 +23,7 @@ public:
static QSharedPointer<PrivilegedProcess> CreatePrivilegedProcess();
bool isSocketConnected() const;
void closeAndResetInstance(bool deleteSelf = false);
signals:
+1 -1
View File
@@ -170,7 +170,7 @@ int NetworkUtilities::AdapterIndexTo(const QHostAddress& dst) {
#ifdef Q_OS_WIN
qDebug() << "Getting Current Internet Adapter that routes to"
<< dst.toString();
quint32_be ipBigEndian;
quint32 ipBigEndian;
quint32 ip = dst.toIPv4Address();
qToBigEndian(ip, &ipBigEndian);
_MIB_IPFORWARDROW routeInfo;
+159
View File
@@ -0,0 +1,159 @@
#include "osSignalHandler.h"
#include <QCoreApplication>
#include <QSocketNotifier>
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
#include <pthread.h>
#include <signal.h>
#include <sys/signalfd.h>
#include <unistd.h>
#elif defined(Q_OS_MACOS)
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>
#endif
#ifdef Q_OS_WIN
#include <QMetaObject>
#include <windows.h>
#endif
namespace
{
static bool initialized = false;
#ifdef Q_OS_WIN
static BOOL WINAPI consoleHandler(DWORD signal)
{
switch (signal) {
case CTRL_CLOSE_EVENT:
case CTRL_C_EVENT:
case CTRL_BREAK_EVENT:
case CTRL_LOGOFF_EVENT:
case CTRL_SHUTDOWN_EVENT:
if (QCoreApplication::instance()) {
QMetaObject::invokeMethod(QCoreApplication::instance(), "quit", Qt::QueuedConnection);
}
return TRUE;
default: return FALSE;
}
}
#endif
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
static int signalFd = -1;
static QSocketNotifier *socketNotifier = nullptr;
static void setupUnixSignalHandler()
{
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigaddset(&set, SIGTERM);
pthread_sigmask(SIG_BLOCK, &set, nullptr);
signalFd = signalfd(-1, &set, SFD_NONBLOCK | SFD_CLOEXEC);
if (signalFd < 0)
return;
socketNotifier = new QSocketNotifier(signalFd, QSocketNotifier::Read, QCoreApplication::instance());
QObject::connect(socketNotifier, &QSocketNotifier::activated, QCoreApplication::instance(), [](int) {
signalfd_siginfo fdsi;
::read(signalFd, &fdsi, sizeof(fdsi));
if (fdsi.ssi_signo == SIGINT || fdsi.ssi_signo == SIGTERM) {
QCoreApplication::quit();
}
});
}
#elif defined(Q_OS_MACX)
static int signalPipe[2] = { -1, -1 };
static QSocketNotifier *socketNotifier = nullptr;
static void macSignalHandler(int)
{
const char ch = 1;
::write(signalPipe[1], &ch, sizeof(ch));
}
static void setupUnixSignalHandler()
{
if (::pipe(signalPipe) != 0)
return;
::fcntl(signalPipe[0], F_SETFL, O_NONBLOCK);
::fcntl(signalPipe[1], F_SETFL, O_NONBLOCK);
struct sigaction sa {};
sa.sa_handler = macSignalHandler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, nullptr);
sigaction(SIGTERM, &sa, nullptr);
socketNotifier = new QSocketNotifier(signalPipe[0], QSocketNotifier::Read, QCoreApplication::instance());
QObject::connect(socketNotifier, &QSocketNotifier::activated, QCoreApplication::instance(), [](int) {
char buf[16];
::read(signalPipe[0], buf, sizeof(buf));
QCoreApplication::quit();
});
}
#endif
static void cleanupUnixSignalHandler()
{
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
if (socketNotifier) {
socketNotifier->setEnabled(false);
}
if (signalFd >= 0) {
::close(signalFd);
signalFd = -1;
}
#elif defined(Q_OS_MACOS)
if (socketNotifier) {
socketNotifier->setEnabled(false);
}
if (signalPipe[0] >= 0) {
::close(signalPipe[0]);
signalPipe[0] = -1;
}
if (signalPipe[1] >= 0) {
::close(signalPipe[1]);
signalPipe[1] = -1;
}
#endif
}
}
OsSignalHandler::OsSignalHandler(QObject *parent) : QObject(parent)
{
}
void OsSignalHandler::setup()
{
if (initialized)
return;
initialized = true;
#if (defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)) || defined(Q_OS_MACX)
setupUnixSignalHandler();
#endif
#ifdef Q_OS_WIN
SetConsoleCtrlHandler(consoleHandler, TRUE);
#endif
QObject::connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, [] { cleanupUnixSignalHandler(); });
}
+17
View File
@@ -0,0 +1,17 @@
#ifndef OSSIGNALHANDLER_H
#define OSSIGNALHANDLER_H
#include <QObject>
class OsSignalHandler : public QObject
{
Q_OBJECT
public:
static void setup();
private:
explicit OsSignalHandler(QObject *parent = nullptr);
static void handleSignal(int signal);
};
#endif // OSSIGNALHANDLER_H
@@ -21,6 +21,7 @@ namespace amnezia::serialization
namespace vless
{
QJsonObject Deserialize(const QString &vless, QString *alias, QString *errMessage);
const QString Serialize(const VlessServerObject &server, const QString &alias);
} // namespace vless
namespace ss
+19
View File
@@ -42,6 +42,25 @@ struct VMessServerObject
};
struct VlessServerObject
{
QString address;
QString id; // UUID
int port;
QString flow = "xtls-rprx-vision";
QString encryption = "none";
QString network = "tcp";
QString security = "reality";
QString serverName; // SNI
QString publicKey;
QString shortId;
QString fingerprint = "chrome";
QString spiderX = "";
JSONSTRUCT_COMPARE(VlessServerObject, address, id, port, flow, encryption)
JSONSTRUCT_REGISTER(VlessServerObject, F(address, id, port, flow, encryption, network, security, serverName, publicKey, shortId, fingerprint, spiderX))
};
namespace transfer
{
+61 -1
View File
@@ -252,5 +252,65 @@ QJsonObject Deserialize(const QString &str, QString *alias, QString *errMessage)
root["inbounds"] = QJsonArray { inbound };
return root;
}
} // namespace amnezia::serialization::vless
const QString Serialize(const VlessServerObject &server, const QString &alias)
{
QUrl url;
// Set basic URL components
url.setScheme("vless");
url.setUserInfo(server.id);
url.setHost(server.address);
url.setPort(server.port);
QUrlQuery query;
if (!server.network.isEmpty() && server.network != "tcp") {
query.addQueryItem("type", server.network);
}
if (!server.encryption.isEmpty()) {
query.addQueryItem("encryption", server.encryption);
}
if (!server.security.isEmpty() && server.security != "none") {
query.addQueryItem("security", server.security);
}
if (!server.flow.isEmpty() && (server.security == "xtls" || server.security == "reality")) {
query.addQueryItem("flow", server.flow);
}
if (!server.serverName.isEmpty()) {
query.addQueryItem("sni", server.serverName);
}
if (server.security == "reality") {
if (!server.fingerprint.isEmpty()) {
query.addQueryItem("fp", server.fingerprint);
}
if (!server.publicKey.isEmpty()) {
query.addQueryItem("pbk", server.publicKey);
}
if (!server.shortId.isEmpty()) {
query.addQueryItem("sid", server.shortId);
}
if (!server.spiderX.isEmpty()) {
query.addQueryItem("spiderX", server.spiderX);
}
}
url.setQuery(query);
if (!alias.isEmpty()) {
url.setFragment(alias);
}
return url.toString(QUrl::ComponentFormattingOption::FullyEncoded);
}
}
+1 -1
View File
@@ -8,7 +8,7 @@
#include <QList>
#include <QMap>
#include <QString>
#include <QMap>
#include "ipaddress.h"
class QJsonObject;
+14
View File
@@ -0,0 +1,14 @@
<svg width="24" height="24" viewBox="0 0 74 74" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_4_34)">
<path d="M55.5 12.3333H18.5C15.0942 12.3333 12.3333 15.0943 12.3333 18.5V55.5C12.3333 58.9058 15.0942 61.6667 18.5 61.6667H55.5C58.9057 61.6667 61.6666 58.9058 61.6666 55.5V18.5C61.6666 15.0943 58.9057 12.3333 55.5 12.3333Z" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21.5833 24.6667H52.4167" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21.5833 37H52.4167" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21.5833 49.3333H40.0833" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="61.5" cy="12.5" r="15" fill="#FBB36B" stroke="#1C1D21" stroke-width="5"/>
</g>
<defs>
<clipPath id="clip0_4_34">
<rect width="74" height="74" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 982 B

+8
View File
@@ -0,0 +1,8 @@
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#CBCAC8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<!-- Основа газеты -->
<rect x="4" y="4" width="16" height="16" rx="2"/>
<!-- Линии текста -->
<line x1="7" y1="8" x2="17" y2="8"/>
<line x1="7" y1="12" x2="17" y2="12"/>
<line x1="7" y1="16" x2="13" y2="16"/>
</svg>

After

Width:  |  Height:  |  Size: 410 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.9 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 35 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="17.5" cy="17.5" r="15" fill="#FBB36B" stroke="#1C1D21" stroke-width="5"/>
</svg>

After

Width:  |  Height:  |  Size: 188 B

+26 -2
View File
@@ -32,17 +32,41 @@
<false/>
<key>UILaunchStoryboardName</key>
<string>AmneziaVPNLaunchScreen</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>QIOSWindowSceneDelegate</string>
</dict>
</array>
</dict>
</dict>
<key>UIRequiredDeviceCapabilities</key>
<array/>
<key>UIRequiresFullScreen</key>
<true/>
<false/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array/>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
<key>com.wireguard.ios.app_group_id</key>
+2
View File
@@ -2,6 +2,7 @@
#include <QTimer>
#include "amnezia_application.h"
#include "core/osSignalHandler.h"
#include "migrations.h"
#include "version.h"
@@ -44,6 +45,7 @@ int main(int argc, char *argv[])
#endif
AmneziaApplication app(argc, argv);
OsSignalHandler::setup();
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
if (isAnotherInstanceRunning()) {
+31 -20
View File
@@ -5,6 +5,9 @@
#include <stdint.h>
#include <QCoreApplication>
#include <QDateTime>
#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include <QHostAddress>
@@ -12,12 +15,13 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QLocalSocket>
#include <QObject>
#include <QStandardPaths>
#include <QTimer>
#include "ipaddress.h"
#include "leakdetector.h"
#include "logger.h"
#include "models/server.h"
#include "daemon/daemonerrors.h"
#include "protocols/protocols_defs.h"
@@ -115,7 +119,6 @@ void LocalSocketController::daemonConnected() {
}
void LocalSocketController::activate(const QJsonObject &rawConfig) {
QString protocolName = rawConfig.value("protocol").toString();
int splitTunnelType = rawConfig.value("splitTunnelType").toInt();
@@ -132,13 +135,16 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
// json.insert("hopindex", QJsonValue((double)hop.m_hopindex));
json.insert("privateKey", wgConfig.value(amnezia::config_key::client_priv_key));
json.insert("deviceIpv4Address", wgConfig.value(amnezia::config_key::client_ip));
m_deviceIpv4 = wgConfig.value(amnezia::config_key::client_ip).toString();
// set up IPv6 unique-local-address, ULA, with "fd00::/8" prefix, not globally routable.
// this will be default IPv6 gateway, OS recognizes that IPv6 link is local and switches to IPv4.
// Otherwise some OSes (Linux) try IPv6 forever and hang.
// https://en.wikipedia.org/wiki/Unique_local_address (RFC 4193)
// https://man7.org/linux/man-pages/man5/gai.conf.5.html
json.insert("deviceIpv6Address", "fd58:baa6:dead::1"); // simply "dead::1" is globally-routable, don't use it
// simply "dead::1" is globally-routable, don't use it
json.insert("deviceIpv6Address", "fd58:baa6:dead::1");
json.insert("serverPublicKey", wgConfig.value(amnezia::config_key::server_pub_key));
json.insert("serverPskKey", wgConfig.value(amnezia::config_key::psk_key));
@@ -220,7 +226,6 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
json.insert("allowedIPAddressRanges", jsAllowedIPAddesses);
QJsonArray jsExcludedAddresses;
jsExcludedAddresses.append(wgConfig.value(amnezia::config_key::hostName));
if (splitTunnelType == 2) {
@@ -264,13 +269,13 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
&& !wgConfig.value(amnezia::config_key::junkPacketMaxSize).isUndefined()
&& !wgConfig.value(amnezia::config_key::initPacketJunkSize).isUndefined()
&& !wgConfig.value(amnezia::config_key::responsePacketJunkSize).isUndefined()
&& !wgConfig.value(amnezia::config_key::cookieReplyPacketJunkSize).isUndefined()
&& !wgConfig.value(amnezia::config_key::transportPacketJunkSize).isUndefined()
// && !wgConfig.value(amnezia::config_key::cookieReplyPacketJunkSize).isUndefined()
// && !wgConfig.value(amnezia::config_key::transportPacketJunkSize).isUndefined()
&& !wgConfig.value(amnezia::config_key::initPacketMagicHeader).isUndefined()
&& !wgConfig.value(amnezia::config_key::responsePacketMagicHeader).isUndefined()
&& !wgConfig.value(amnezia::config_key::underloadPacketMagicHeader).isUndefined()
&& !wgConfig.value(amnezia::config_key::transportPacketMagicHeader).isUndefined()
&& !wgConfig.value(amnezia::config_key::specialJunk1).isUndefined()
/* && !wgConfig.value(amnezia::config_key::specialJunk1).isUndefined()
&& !wgConfig.value(amnezia::config_key::specialJunk2).isUndefined()
&& !wgConfig.value(amnezia::config_key::specialJunk3).isUndefined()
&& !wgConfig.value(amnezia::config_key::specialJunk4).isUndefined()
@@ -278,27 +283,27 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
&& !wgConfig.value(amnezia::config_key::controlledJunk1).isUndefined()
&& !wgConfig.value(amnezia::config_key::controlledJunk2).isUndefined()
&& !wgConfig.value(amnezia::config_key::controlledJunk3).isUndefined()
&& !wgConfig.value(amnezia::config_key::specialHandshakeTimeout).isUndefined()) {
&& !wgConfig.value(amnezia::config_key::specialHandshakeTimeout).isUndefined()*/) {
json.insert(amnezia::config_key::junkPacketCount, wgConfig.value(amnezia::config_key::junkPacketCount));
json.insert(amnezia::config_key::junkPacketMinSize, wgConfig.value(amnezia::config_key::junkPacketMinSize));
json.insert(amnezia::config_key::junkPacketMaxSize, wgConfig.value(amnezia::config_key::junkPacketMaxSize));
json.insert(amnezia::config_key::initPacketJunkSize, wgConfig.value(amnezia::config_key::initPacketJunkSize));
json.insert(amnezia::config_key::responsePacketJunkSize, wgConfig.value(amnezia::config_key::responsePacketJunkSize));
json.insert(amnezia::config_key::cookieReplyPacketJunkSize, wgConfig.value(amnezia::config_key::cookieReplyPacketJunkSize));
json.insert(amnezia::config_key::transportPacketJunkSize, wgConfig.value(amnezia::config_key::transportPacketJunkSize));
// json.insert(amnezia::config_key::cookieReplyPacketJunkSize, wgConfig.value(amnezia::config_key::cookieReplyPacketJunkSize));
// json.insert(amnezia::config_key::transportPacketJunkSize, wgConfig.value(amnezia::config_key::transportPacketJunkSize));
json.insert(amnezia::config_key::initPacketMagicHeader, wgConfig.value(amnezia::config_key::initPacketMagicHeader));
json.insert(amnezia::config_key::responsePacketMagicHeader, wgConfig.value(amnezia::config_key::responsePacketMagicHeader));
json.insert(amnezia::config_key::underloadPacketMagicHeader, wgConfig.value(amnezia::config_key::underloadPacketMagicHeader));
json.insert(amnezia::config_key::transportPacketMagicHeader, wgConfig.value(amnezia::config_key::transportPacketMagicHeader));
json.insert(amnezia::config_key::specialJunk1, wgConfig.value(amnezia::config_key::specialJunk1));
json.insert(amnezia::config_key::specialJunk2, wgConfig.value(amnezia::config_key::specialJunk2));
json.insert(amnezia::config_key::specialJunk3, wgConfig.value(amnezia::config_key::specialJunk3));
json.insert(amnezia::config_key::specialJunk4, wgConfig.value(amnezia::config_key::specialJunk4));
json.insert(amnezia::config_key::specialJunk5, wgConfig.value(amnezia::config_key::specialJunk5));
json.insert(amnezia::config_key::controlledJunk1, wgConfig.value(amnezia::config_key::controlledJunk1));
json.insert(amnezia::config_key::controlledJunk2, wgConfig.value(amnezia::config_key::controlledJunk2));
json.insert(amnezia::config_key::controlledJunk3, wgConfig.value(amnezia::config_key::controlledJunk3));
json.insert(amnezia::config_key::specialHandshakeTimeout, wgConfig.value(amnezia::config_key::specialHandshakeTimeout));
// json.insert(amnezia::config_key::specialJunk1, wgConfig.value(amnezia::config_key::specialJunk1));
// json.insert(amnezia::config_key::specialJunk2, wgConfig.value(amnezia::config_key::specialJunk2));
// json.insert(amnezia::config_key::specialJunk3, wgConfig.value(amnezia::config_key::specialJunk3));
// json.insert(amnezia::config_key::specialJunk4, wgConfig.value(amnezia::config_key::specialJunk4));
// json.insert(amnezia::config_key::specialJunk5, wgConfig.value(amnezia::config_key::specialJunk5));
// json.insert(amnezia::config_key::controlledJunk1, wgConfig.value(amnezia::config_key::controlledJunk1));
// json.insert(amnezia::config_key::controlledJunk2, wgConfig.value(amnezia::config_key::controlledJunk2));
// json.insert(amnezia::config_key::controlledJunk3, wgConfig.value(amnezia::config_key::controlledJunk3));
// json.insert(amnezia::config_key::specialHandshakeTimeout, wgConfig.value(amnezia::config_key::specialHandshakeTimeout));
}
write(json);
@@ -449,6 +454,7 @@ void LocalSocketController::parseCommand(const QByteArray& command) {
}
if (type == "status") {
QJsonValue serverIpv4Gateway = obj.value("serverIpv4Gateway");
if (!serverIpv4Gateway.isString()) {
logger.error() << "Unexpected serverIpv4Gateway value";
@@ -493,6 +499,11 @@ void LocalSocketController::parseCommand(const QByteArray& command) {
logger.debug() << "Handshake completed with:"
<< pubkey.toString();
checkStatus();
emit statusUpdated("", m_deviceIpv4, 0, 0);
emit connected(pubkey.toString());
return;
}
+2
View File
@@ -12,6 +12,7 @@
#include "controllerimpl.h"
class QJsonObject;
class LocalSocketController final : public ControllerImpl {
@@ -58,6 +59,7 @@ class LocalSocketController final : public ControllerImpl {
QByteArray m_buffer;
QString m_deviceIpv4;
std::function<void(const QString&)> m_logCallback = nullptr;
QTimer m_initializingTimer;
+22 -37
View File
@@ -11,7 +11,6 @@
#include "logger.h"
//#include "mozillavpn.h"
#include "networkwatcherimpl.h"
#include "platforms/dummy/dummynetworkwatcher.h"
//#include "settingsholder.h"
#ifdef MZ_WINDOWS
@@ -51,7 +50,7 @@ NetworkWatcher::NetworkWatcher() { MZ_COUNT_CTOR(NetworkWatcher); }
NetworkWatcher::~NetworkWatcher() { MZ_COUNT_DTOR(NetworkWatcher); }
void NetworkWatcher::initialize() {
logger.debug() << "Initialize";
logger.debug() << "Initialize NetworkWatcher";
#if defined(MZ_WINDOWS)
m_impl = new WindowsNetworkWatcher(this);
@@ -69,59 +68,45 @@ void NetworkWatcher::initialize() {
m_impl = new DummyNetworkWatcher(this);
#endif
connect(m_impl, &NetworkWatcherImpl::unsecuredNetwork, this,
&NetworkWatcher::unsecuredNetwork);
connect(m_impl, &NetworkWatcherImpl::networkChanged, this,
&NetworkWatcher::networkChange);
connect(m_impl, &NetworkWatcherImpl::sleepMode, this,
&NetworkWatcher::onSleepMode);
m_impl->initialize();
// TODO: IMPL FOR AMNEZIA
#if 0
SettingsHolder* settingsHolder = SettingsHolder::instance();
Q_ASSERT(settingsHolder);
m_active = settingsHolder->unsecuredNetworkAlert() ||
settingsHolder->captivePortalAlert();
m_reportUnsecuredNetwork = settingsHolder->unsecuredNetworkAlert();
if (m_active) {
// Enable sleep/wake monitoring for VPN auto-reconnection
logger.debug() << "Starting NetworkWatcher for sleep/wake monitoring";
logger.debug() << "About to call m_impl->start()";
try {
m_impl->start();
logger.debug() << "m_impl->start() completed successfully";
} catch (const std::exception& e) {
logger.error() << "Exception in m_impl->start():" << e.what();
} catch (...) {
logger.error() << "Unknown exception in m_impl->start()";
}
connect(settingsHolder, &SettingsHolder::unsecuredNetworkAlertChanged, this,
&NetworkWatcher::settingsChanged);
connect(settingsHolder, &SettingsHolder::captivePortalAlertChanged, this,
&NetworkWatcher::settingsChanged);
#endif
m_active = true;
m_reportUnsecuredNetwork = false; // Disable unsecured network alerts for Amnezia
}
void NetworkWatcher::settingsChanged() {
// TODO: IMPL FOR AMNEZIA
#if 0
SettingsHolder* settingsHolder = SettingsHolder::instance();
m_active = settingsHolder->unsecuredNetworkAlert() ||
settingsHolder->captivePortalAlert();
m_reportUnsecuredNetwork = settingsHolder->unsecuredNetworkAlert();
// For Amnezia: Keep NetworkWatcher always active for sleep/wake monitoring
logger.debug() << "NetworkWatcher settings changed - keeping sleep monitoring active";
}
if (m_active) {
logger.debug()
<< "Starting Network Watcher; Reporting of Unsecured Networks: "
<< m_reportUnsecuredNetwork;
m_impl->start();
} else {
logger.debug() << "Stopping Network Watcher";
m_impl->stop();
}
#endif
void NetworkWatcher::onSleepMode()
{
logger.debug() << "Resumed from sleep mode";
emit sleepMode();
}
void NetworkWatcher::unsecuredNetwork(const QString& networkName,
const QString& networkId) {
logger.debug() << "Unsecured network:" << logger.sensitive(networkName)
<< "id:" << logger.sensitive(networkId);
#ifndef UNIT_TEST
if (!m_reportUnsecuredNetwork) {
logger.debug() << "Disabled. Ignoring unsecured network";
+3
View File
@@ -29,10 +29,13 @@ public:
// false to restore.
void simulateDisconnection(bool simulatedDisconnection);
void onSleepMode();
QNetworkInformation::Reachability getReachability();
signals:
void networkChange();
void sleepMode();
private:
void settingsChanged();
+2
View File
@@ -41,6 +41,8 @@ signals:
// TODO: Only windows-networkwatcher has this, the other plattforms should
// too.
void networkChanged(QString newBSSID);
void sleepMode();
private:
bool m_active = false;
+5 -2
View File
@@ -41,6 +41,7 @@ void PingHelper::start(const QString& serverIpv4Gateway,
m_gateway = QHostAddress(serverIpv4Gateway);
m_source = QHostAddress(deviceIpv4Address.section('/', 0, 0));
m_pingSender = PingSenderFactory::create(m_source, this);
// Some platforms require root access to send and receive ICMP pings. If
@@ -53,8 +54,10 @@ void PingHelper::start(const QString& serverIpv4Gateway,
connect(m_pingSender, &PingSender::recvPing, this, &PingHelper::pingReceived,
Qt::QueuedConnection);
connect(m_pingSender, &PingSender::criticalPingError, this,
[]() { logger.info() << "Encountered Unrecoverable ping error"; });
connect(m_pingSender, &PingSender::criticalPingError, this, [this]() {
logger.info() << "Encountered Unrecoverable ping error";
emit connectionLose();
});
// Reset the ping statistics
m_sequence = 0;
+2
View File
@@ -33,6 +33,8 @@ class PingHelper final : public QObject {
signals:
void pingSentAndReceived(qint64 msec);
void connectionLose();
private:
void nextPing();
+10 -11
View File
@@ -5,27 +5,26 @@
#include "pingsenderfactory.h"
#if defined(MZ_LINUX) || defined(MZ_ANDROID)
//# include "platforms/linux/linuxpingsender.h"
# include "platforms/linux/linuxpingsender.h"
#elif defined(MZ_MACOS) || defined(MZ_IOS)
# include "platforms/macos/macospingsender.h"
# include "platforms/macos/macospingsender.h"
#elif defined(MZ_WINDOWS)
# include "platforms/windows/windowspingsender.h"
#elif defined(MZ_DUMMY) || defined(UNIT_TEST)
# include "platforms/dummy/dummypingsender.h"
# include "platforms/windows/windowspingsender.h"
#elif defined(MZ_WASM) || defined(UNIT_TEST)
# include "platforms/dummy/dummypingsender.h"
#else
# error "Unsupported platform"
# error "Unsupported platform"
#endif
PingSender* PingSenderFactory::create(const QHostAddress& source,
QObject* parent) {
#if defined(MZ_LINUX) || defined(MZ_ANDROID)
return nullptr;
// return new LinuxPingSender(source, parent);
return new LinuxPingSender(source, parent);
#elif defined(MZ_MACOS) || defined(MZ_IOS)
return new MacOSPingSender(source, parent);
return new MacOSPingSender(source, parent);
#elif defined(MZ_WINDOWS)
return new WindowsPingSender(source, parent);
return new WindowsPingSender(source, parent);
#else
return new DummyPingSender(source, parent);
return new DummyPingSender(source, parent);
#endif
}
+4 -3
View File
@@ -10,9 +10,10 @@ class QHostAddress;
class QObject;
class PingSenderFactory final {
public:
PingSenderFactory() = delete;
static PingSender* create(const QHostAddress& source, QObject* parent);
public:
PingSenderFactory() = delete;
static PingSender* create(const QHostAddress& source, QObject* parent);
};
#endif // PINGSENDERFACTORY_H
@@ -99,7 +99,9 @@ bool AndroidController::initialize()
{"onFileOpened", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onFileOpened)},
{"onConfigImported", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onConfigImported)},
{"onAuthResult", "(Z)V", reinterpret_cast<void *>(onAuthResult)},
{"decodeQrCode", "(Ljava/lang/String;)Z", reinterpret_cast<bool *>(decodeQrCode)}
{"decodeQrCode", "(Ljava/lang/String;)Z", reinterpret_cast<bool *>(decodeQrCode)},
{"onImeInsetsChanged", "(I)V", reinterpret_cast<void *>(onImeInsetsChanged)},
{"onSystemBarsInsetsChanged", "(II)V", reinterpret_cast<void *>(onSystemBarsInsetsChanged)}
};
QJniEnvironment env;
@@ -202,6 +204,21 @@ bool AndroidController::isOnTv()
return callActivityMethod<jboolean>("isOnTv", "()Z");
}
bool AndroidController::isEdgeToEdgeEnabled()
{
return callActivityMethod<jboolean>("isEdgeToEdgeEnabled", "()Z");
}
int AndroidController::getStatusBarHeight()
{
return callActivityMethod<jint>("getStatusBarHeight", "()I");
}
int AndroidController::getNavigationBarHeight()
{
return callActivityMethod<jint>("getNavigationBarHeight", "()I");
}
void AndroidController::startQrReaderActivity()
{
callActivityMethod("startQrCodeReader", "()V");
@@ -521,3 +538,23 @@ bool AndroidController::decodeQrCode(JNIEnv *env, jobject thiz, jstring data)
return ImportController::decodeQrCode(AndroidUtils::convertJString(env, data));
}
// static
void AndroidController::onImeInsetsChanged(JNIEnv *env, jobject thiz, jint heightDp)
{
Q_UNUSED(env);
Q_UNUSED(thiz);
qDebug() << "Android IME insets changed: height =" << heightDp << "dp";
emit AndroidController::instance()->imeInsetsChanged(heightDp);
}
// static
void AndroidController::onSystemBarsInsetsChanged(JNIEnv *env, jobject thiz, jint navBarHeightDp, jint statusBarHeightDp)
{
Q_UNUSED(env);
Q_UNUSED(thiz);
qDebug() << "Android system bars insets changed: nav bar =" << navBarHeightDp << "dp, status bar =" << statusBarHeightDp << "dp";
emit AndroidController::instance()->systemBarsInsetsChanged(navBarHeightDp, statusBarHeightDp);
}
@@ -39,6 +39,9 @@ public:
QString getFileName(const QString &uri);
bool isCameraPresent();
bool isOnTv();
bool isEdgeToEdgeEnabled();
int getStatusBarHeight();
int getNavigationBarHeight();
void startQrReaderActivity();
void setSaveLogs(bool enabled);
void exportLogsFile(const QString &fileName);
@@ -70,6 +73,8 @@ signals:
void importConfigFromOutside(QString config);
void initConnectionState(Vpn::ConnectionState state);
void authenticationResult(bool result);
void imeInsetsChanged(int heightDp);
void systemBarsInsetsChanged(int navBarHeightDp, int statusBarHeightDp);
private:
bool isWaitingStatus = true;
@@ -98,6 +103,8 @@ private:
static void onFileOpened(JNIEnv *env, jobject thiz, jstring uri);
static void onAuthResult(JNIEnv *env, jobject thiz, jboolean result);
static bool decodeQrCode(JNIEnv *env, jobject thiz, jstring data);
static void onImeInsetsChanged(JNIEnv *env, jobject thiz, jint heightDp);
static void onSystemBarsInsetsChanged(JNIEnv *env, jobject thiz, jint navBarHeightDp, jint statusBarHeightDp);
template <typename Ret, typename ...Args>
static auto callActivityMethod(const char *methodName, const char *signature, Args &&...args);
@@ -0,0 +1,82 @@
#import <UIKit/UIKit.h>
#import <objc/runtime.h>
#include <dispatch/dispatch.h>
#include <QByteArray>
#include <QFile>
#include <QString>
#include "ios_controller.h"
using SceneOpenURLContexts = void (*)(id, SEL, UIScene *, NSSet<UIOpenURLContext *> *);
static SceneOpenURLContexts g_originalSceneOpenURLContexts = nullptr;
static void amnezia_handleURL(NSURL *url)
{
if (!url || !url.isFileURL) {
return;
}
QString filePath(url.path.UTF8String);
if (filePath.isEmpty()) {
return;
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (filePath.contains("backup")) {
IosController::Instance()->importBackupFromOutside(filePath);
return;
}
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly)) {
return;
}
const QByteArray data = file.readAll();
IosController::Instance()->importConfigFromOutside(QString::fromUtf8(data));
});
}
static void amnezia_scene_openURLContexts(id self, SEL _cmd, UIScene *scene, NSSet<UIOpenURLContext *> *contexts)
{
if (g_originalSceneOpenURLContexts) {
g_originalSceneOpenURLContexts(self, _cmd, scene, contexts);
}
if (!contexts || contexts.count == 0) {
return;
}
if (@available(iOS 13.0, *)) {
for (UIOpenURLContext *context in contexts) {
amnezia_handleURL(context.URL);
}
}
}
@interface AmneziaSceneDelegateHooks : NSObject
@end
@implementation AmneziaSceneDelegateHooks
+ (void)load
{
Class cls = objc_getClass("QIOSWindowSceneDelegate");
if (!cls) {
return;
}
SEL selector = @selector(scene:openURLContexts:);
Method method = class_getInstanceMethod(cls, selector);
if (method) {
g_originalSceneOpenURLContexts = reinterpret_cast<SceneOpenURLContexts>(method_getImplementation(method));
method_setImplementation(method, reinterpret_cast<IMP>(amnezia_scene_openURLContexts));
} else {
const char *types = "v@:@@";
class_addMethod(cls, selector, reinterpret_cast<IMP>(amnezia_scene_openURLContexts), types);
}
}
@end
+35 -3
View File
@@ -2,7 +2,8 @@ import Foundation
import os.log
struct Log {
static let osLog = Logger()
private static let subsystemIdentifier = Bundle.main.bundleIdentifier ?? "org.amnezia.AmneziaVPN"
static let osLog = Logger(subsystem: subsystemIdentifier, category: "App")
private static let IsLoggingEnabledKey = "IsLoggingEnabled"
static var isLoggingEnabled: Bool {
@@ -77,9 +78,40 @@ struct Log {
static func log(_ type: OSLogType, title: String = "", message: String, url: URL = neLogURL) {
NSLog("\(title) \(message)")
guard isLoggingEnabled else { return }
switch type {
case .debug:
if title.isEmpty {
osLog.debug("\(message, privacy: .public)")
} else {
osLog.debug("\(title, privacy: .public) \(message, privacy: .public)")
}
case .info:
if title.isEmpty {
osLog.info("\(message, privacy: .public)")
} else {
osLog.info("\(title, privacy: .public) \(message, privacy: .public)")
}
case .error:
if title.isEmpty {
osLog.error("\(message, privacy: .public)")
} else {
osLog.error("\(title, privacy: .public) \(message, privacy: .public)")
}
case .fault:
if title.isEmpty {
osLog.fault("\(message, privacy: .public)")
} else {
osLog.fault("\(title, privacy: .public) \(message, privacy: .public)")
}
default:
if title.isEmpty {
osLog.log("\(message, privacy: .public)")
} else {
osLog.log("\(title, privacy: .public) \(message, privacy: .public)")
}
}
osLog.log(level: type, "\(title) \(message)")
guard isLoggingEnabled else { return }
let date = Date()
let level = Record.Level(from: type)
+55 -1
View File
@@ -1,22 +1,76 @@
import Foundation
import os.log
private let subsystemIdentifier = Bundle.main.bundleIdentifier ?? "org.amnezia.AmneziaVPN"
private let wireGuardSystemLogger = Logger(subsystem: subsystemIdentifier, category: "WireGuard")
private let openVPNSystemLogger = Logger(subsystem: subsystemIdentifier, category: "OpenVPN")
private let xraySystemLogger = Logger(subsystem: subsystemIdentifier, category: "Xray")
private let networkExtensionLogger = Logger(subsystem: subsystemIdentifier, category: "NetworkExtension")
private func logToSystem(_ logger: Logger, type: OSLogType, prefix: String, title: String, message: String) {
let combinedTitle: String
if title.isEmpty {
combinedTitle = prefix
} else {
combinedTitle = "\(prefix): \(title)"
}
switch type {
case .debug:
if combinedTitle.isEmpty {
logger.debug("\(message, privacy: .public)")
} else {
logger.debug("\(combinedTitle, privacy: .public) \(message, privacy: .public)")
}
case .info:
if combinedTitle.isEmpty {
logger.info("\(message, privacy: .public)")
} else {
logger.info("\(combinedTitle, privacy: .public) \(message, privacy: .public)")
}
case .error:
if combinedTitle.isEmpty {
logger.error("\(message, privacy: .public)")
} else {
logger.error("\(combinedTitle, privacy: .public) \(message, privacy: .public)")
}
case .fault:
if combinedTitle.isEmpty {
logger.fault("\(message, privacy: .public)")
} else {
logger.fault("\(combinedTitle, privacy: .public) \(message, privacy: .public)")
}
default:
if combinedTitle.isEmpty {
logger.log("\(message, privacy: .public)")
} else {
logger.log("\(combinedTitle, privacy: .public) \(message, privacy: .public)")
}
}
}
public func wg_log(_ type: OSLogType, title: String = "", staticMessage: StaticString) {
neLog(type, title: "WG: \(title)", message: "\(staticMessage)")
let stringMessage = String(describing: staticMessage)
logToSystem(wireGuardSystemLogger, type: type, prefix: "WG", title: title, message: stringMessage)
neLog(type, title: "WG: \(title)", message: stringMessage)
}
public func wg_log(_ type: OSLogType, title: String = "", message: String) {
logToSystem(wireGuardSystemLogger, type: type, prefix: "WG", title: title, message: message)
neLog(type, title: "WG: \(title)", message: message)
}
public func ovpnLog(_ type: OSLogType, title: String = "", message: String) {
logToSystem(openVPNSystemLogger, type: type, prefix: "OVPN", title: title, message: message)
neLog(type, title: "OVPN: \(title)", message: message)
}
public func xrayLog(_ type: OSLogType, title: String = "", message: String) {
logToSystem(xraySystemLogger, type: type, prefix: "XRAY", title: title, message: message)
neLog(type, title: "XRAY: \(title)", message: message)
}
public func neLog(_ type: OSLogType, title: String = "", message: String) {
logToSystem(networkExtensionLogger, type: type, prefix: "NE", title: title, message: message)
Log.log(type, title: "NE: \(title)", message: message)
}
@@ -1,6 +1,7 @@
import Foundation
import NetworkExtension
import OpenVPNAdapter
import CryptoKit
struct OpenVPNConfig: Decodable {
let config: String
@@ -27,26 +28,83 @@ extension PacketTunnelProvider {
let ovpnConfiguration = Data(openVPNConfig.config.utf8)
setupAndlaunchOpenVPN(withConfig: ovpnConfiguration, completionHandler: completionHandler)
} catch {
ovpnLog(.error, message: "Can't parse config: \(error.localizedDescription)")
if let underlyingError = (error as NSError).userInfo[NSUnderlyingErrorKey] as? NSError {
ovpnLog(.error, message: "Can't parse config: \(underlyingError.localizedDescription)")
}
ovpnLog(.error, message: "Can't parse OpenVPN config: \(error.localizedDescription)")
return
}
}
private func logOpenVPNError(_ error: NSError) {
let fatalFlag = (error.userInfo[OpenVPNAdapterErrorFatalKey] as? Bool) ?? false
var lines: [String] = []
lines.append("domain=\(error.domain) code=\(error.code) fatal=\(fatalFlag)")
if let adapterMessage = error.userInfo[OpenVPNAdapterErrorMessageKey] as? String, !adapterMessage.isEmpty {
lines.append("message=\(adapterMessage)")
}
let userInfoKeys = error.userInfo.keys.map { String(describing: $0) }.sorted()
if !userInfoKeys.isEmpty {
lines.append("userInfoKeys=[\(userInfoKeys.joined(separator: ","))]")
}
if let underlying = error.userInfo[NSUnderlyingErrorKey] as? NSError {
lines.append("underlying=\(underlying.domain)#\(underlying.code) fatal=\((underlying.userInfo[OpenVPNAdapterErrorFatalKey] as? Bool) ?? false)")
if let underlyingMessage = underlying.userInfo[OpenVPNAdapterErrorMessageKey] as? String, !underlyingMessage.isEmpty {
lines.append("underlyingMessage=\(underlyingMessage)")
} else if !underlying.localizedDescription.isEmpty {
lines.append("underlyingLocalized=\(underlying.localizedDescription)")
}
} else if let underlying = error.userInfo[NSUnderlyingErrorKey] {
lines.append("underlyingRaw=\(underlying)")
}
let formatted = lines.joined(separator: "\n ")
ovpnLog(.error, title: "Error", message: formatted)
}
private func setupAndlaunchOpenVPN(withConfig ovpnConfiguration: Data,
withShadowSocks viaSS: Bool = false,
completionHandler: @escaping (Error?) -> Void) {
ovpnLog(.info, message: "Setup and launch")
let str = String(decoding: ovpnConfiguration, as: UTF8.self)
var configString = String(decoding: ovpnConfiguration, as: UTF8.self)
let digest = SHA256.hash(data: ovpnConfiguration)
let digestString = digest.map { String(format: "%02x", $0) }.joined()
ovpnLog(.info, title: "ConfigDigest", message: digestString)
let hasTlsAuthOpen = configString.contains("<tls-auth>")
let hasTlsAuthClose = configString.contains("</tls-auth>")
ovpnLog(.info, title: "ConfigFlags", message: "tls-auth open=\(hasTlsAuthOpen) close=\(hasTlsAuthClose)")
let lines = configString.split(separator: "\n")
let head = lines.prefix(10).joined(separator: "\n")
let tail = lines.suffix(10).joined(separator: "\n")
ovpnLog(.debug, title: "ConfigHead", message: head)
ovpnLog(.debug, title: "ConfigTail", message: tail)
if let start = configString.range(of: "<tls-auth>"),
let end = configString.range(of: "</tls-auth>", range: start.upperBound..<configString.endIndex) {
let keyBody = String(configString[start.upperBound..<end.lowerBound])
ovpnLog(.debug, title: "TLSAuthInline", message: keyBody)
let sanitizedLines = keyBody
.split(whereSeparator: { $0.isNewline })
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
.filter { !$0.hasPrefix("#") }
let sanitizedKey = sanitizedLines.joined(separator: "\n")
ovpnLog(.debug, title: "TLSAuthSanitized", message: sanitizedKey)
let sanitizedBlock = "<tls-auth>\n\(sanitizedKey)\n</tls-auth>"
configString.replaceSubrange(start.lowerBound..<end.upperBound, with: sanitizedBlock)
}
let normalizedConfig = configString.replacingOccurrences(of: "\r\n", with: "\n")
let sanitizedData = Data(normalizedConfig.utf8)
let configuration = OpenVPNConfiguration()
configuration.fileContent = ovpnConfiguration
if str.contains("cloak") {
configuration.fileContent = sanitizedData
if configString.contains("cloak") {
configuration.setPTCloak()
}
@@ -57,6 +115,8 @@ extension PacketTunnelProvider {
evaluation = try ovpnAdapter?.apply(configuration: configuration)
} catch {
let nsError = error as NSError
ovpnLog(.error, title: "ApplyConfig", message: "domain=\(nsError.domain) code=\(nsError.code) info=\(nsError.userInfo)")
completionHandler(error)
return
}
@@ -208,8 +268,11 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
// Handle errors thrown by the OpenVPN library
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleError error: Error) {
let nsError = error as NSError
logOpenVPNError(nsError)
// Handle only fatal errors
guard let fatal = (error as NSError).userInfo[OpenVPNAdapterErrorFatalKey] as? Bool,
guard let fatal = nsError.userInfo[OpenVPNAdapterErrorFatalKey] as? Bool,
fatal == true else { return }
if vpnReachability.isTracking {
@@ -1,6 +1,5 @@
import Foundation
import NetworkExtension
import WireGuardKitGo
enum XrayErrors: Error {
case noXrayConfig
+37 -3
View File
@@ -29,12 +29,46 @@ const char* MessageKey::SplitTunnelSites = "SplitTunnelSites";
#if !MACOS_NE
static UIViewController* getViewController() {
NSArray *windows = [[UIApplication sharedApplication]windows];
for (UIWindow *window in windows) {
if (window.isKeyWindow) {
UIApplication *application = [UIApplication sharedApplication];
if (@available(iOS 13.0, *)) {
for (UIScene *scene in application.connectedScenes) {
if (scene.activationState != UISceneActivationStateForegroundActive) {
continue;
}
if (![scene isKindOfClass:[UIWindowScene class]]) {
continue;
}
UIWindowScene *windowScene = (UIWindowScene *)scene;
for (UIWindow *window in windowScene.windows) {
if (window.isKeyWindow && window.rootViewController) {
return window.rootViewController;
}
}
for (UIWindow *window in windowScene.windows) {
if (!window.isHidden && window.rootViewController) {
return window.rootViewController;
}
}
}
}
for (UIWindow *window in application.windows) {
if (window.isKeyWindow && window.rootViewController) {
return window.rootViewController;
}
}
for (UIWindow *window in application.windows) {
if (window.rootViewController) {
return window.rootViewController;
}
}
return nil;
}
#endif
@@ -34,6 +34,9 @@ void IOSNetworkWatcher::initialize() {
});
nw_path_monitor_start(m_networkMonitor);
// Call start() to initialize sleep/wake monitoring (will call MacOSNetworkWatcher::start() if this is macOS)
this->start();
//TODO IMPL FOR AMNEZIA
}
@@ -41,6 +41,9 @@ void LinuxNetworkWatcher::initialize() {
connect(m_worker, &LinuxNetworkWatcherWorker::unsecuredNetwork, this,
&LinuxNetworkWatcher::unsecuredNetwork);
connect(m_worker, &LinuxNetworkWatcherWorker::sleepMode, this,
&NetworkWatcherImpl::sleepMode);
// Let's wait a few seconds to allow the UI to be fully loaded and shown.
// This is not strictly needed, but it's better for user experience because
// it makes the UI faster to appear, plus it gives a bit of delay between the
@@ -33,7 +33,21 @@
#define NM_802_11_AP_SEC_WEAK_CRYPTO \
(NM_802_11_AP_SEC_PAIR_WEP40 | NM_802_11_AP_SEC_PAIR_WEP104)
enum NMState {
NM_STATE_UNKNOWN = 0,
NM_STATE_ASLEEP = 10,
NM_STATE_DISCONNECTED = 20,
NM_STATE_DISCONNECTING = 30,
NM_STATE_CONNECTING = 40,
NM_STATE_CONNECTED_LOCAL = 50,
NM_STATE_CONNECTED_SITE = 60,
NM_STATE_CONNECTED_GLOBAL = 70
};
constexpr const char* DBUS_NETWORKMANAGER = "org.freedesktop.NetworkManager";
constexpr const char* DBUS_NETWORKMANAGER_PATH = "/org/freedesktop/NetworkManager";
namespace {
Logger logger("LinuxNetworkWatcherWorker");
@@ -73,7 +87,7 @@ void LinuxNetworkWatcherWorker::initialize() {
// documentation:
// https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.html
QDBusInterface nm(DBUS_NETWORKMANAGER, "/org/freedesktop/NetworkManager",
QDBusInterface nm(DBUS_NETWORKMANAGER, DBUS_NETWORKMANAGER_PATH,
DBUS_NETWORKMANAGER, QDBusConnection::systemBus());
if (!nm.isValid()) {
logger.error()
@@ -108,6 +122,12 @@ void LinuxNetworkWatcherWorker::initialize() {
SLOT(propertyChanged(QString, QVariantMap, QStringList)));
}
QDBusConnection::systemBus().connect(DBUS_NETWORKMANAGER,
DBUS_NETWORKMANAGER_PATH,
DBUS_NETWORKMANAGER,
"StateChanged",
this, SLOT(NMStateChanged(quint32)));
if (m_devicePaths.isEmpty()) {
logger.warning() << "No wifi devices found";
return;
@@ -173,5 +193,16 @@ void LinuxNetworkWatcherWorker::checkDevices() {
emit unsecuredNetwork(ssid, bssid);
break;
}
}
}
void LinuxNetworkWatcherWorker::NMStateChanged(quint32 state)
{
if (state == NM_STATE_ASLEEP) {
emit sleepMode();
}
logger.debug() << "NMStateChanged " << state;
}
@@ -23,6 +23,7 @@ class LinuxNetworkWatcherWorker final : public QObject {
signals:
void unsecuredNetwork(const QString& networkName, const QString& networkId);
void sleepMode();
public slots:
void initialize();
@@ -30,6 +31,7 @@ class LinuxNetworkWatcherWorker final : public QObject {
private slots:
void propertyChanged(QString interface, QVariantMap properties,
QStringList list);
void NMStateChanged(quint32 state);
private:
// We collect the list of DBus wifi network device paths during the
+185
View File
@@ -0,0 +1,185 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "linuxpingsender.h"
#include <arpa/inet.h>
#include <errno.h>
#include <linux/filter.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <sys/socket.h>
#include <unistd.h>
#include <QSocketNotifier>
#include <QtEndian>
#include "leakdetector.h"
#include "logger.h"
#include "qhostaddress.h"
namespace {
Logger logger("LinuxPingSender");
}
int LinuxPingSender::createSocket() {
// Try creating an ICMP socket. This would be the ideal choice, but it can
// fail depending on the kernel config (see: sys.net.ipv4.ping_group_range)
m_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);
if (m_socket >= 0) {
m_ident = 0;
return m_socket;
}
if ((errno != EPERM) && (errno != EACCES)) {
return -1;
}
// As a fallback, create a raw socket, which requires root permissions
// or CAP_NET_RAW to be granted to the VPN client.
m_socket = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (m_socket < 0) {
return -1;
}
m_ident = getpid() & 0xffff;
// Attach a BPF filter to discard everything but replies to our echo.
struct sock_filter bpf_prog[] = {
BPF_STMT(BPF_LDX | BPF_B | BPF_MSH, 0), /* Skip IP header. */
BPF_STMT(BPF_LD | BPF_H | BPF_IND, 4), /* Load icmp echo ident */
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, m_ident, 1, 0), /* Ours? */
BPF_STMT(BPF_RET | BPF_K, 0), /* Unexpected identifier. Reject. */
BPF_STMT(BPF_LD | BPF_B | BPF_IND, 0), /* Load icmp type */
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, ICMP_ECHOREPLY, 1, 0), /* Echo? */
BPF_STMT(BPF_RET | BPF_K, 0), /* Unexpected type. Reject. */
BPF_STMT(BPF_RET | BPF_K, ~0U), /* Packet passes the filter. */
};
struct sock_fprog filter = {
.len = sizeof(bpf_prog) / sizeof(struct sock_filter),
.filter = bpf_prog,
};
setsockopt(m_socket, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter));
return m_socket;
}
LinuxPingSender::LinuxPingSender(const QHostAddress& source, QObject* parent)
: PingSender(parent) {
MZ_COUNT_CTOR(LinuxPingSender);
logger.debug() << "LinuxPingSender(" + logger.sensitive(source.toString()) +
") created";
m_socket = createSocket();
if (m_socket < 0) {
logger.error() << "Socket creation error: " << strerror(errno);
return;
}
quint32 ipv4addr = INADDR_ANY;
if (!source.isNull()) {
ipv4addr = source.toIPv4Address();
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof addr);
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = qToBigEndian<quint32>(ipv4addr);
if (bind(m_socket, (struct sockaddr*)&addr, sizeof(addr)) != 0) {
close(m_socket);
m_socket = -1;
logger.error() << "bind error:" << strerror(errno);
return;
}
m_notifier = new QSocketNotifier(m_socket, QSocketNotifier::Read, this);
if (m_ident) {
connect(m_notifier, &QSocketNotifier::activated, this,
&LinuxPingSender::rawSocketReady);
} else {
connect(m_notifier, &QSocketNotifier::activated, this,
&LinuxPingSender::icmpSocketReady);
}
}
LinuxPingSender::~LinuxPingSender() {
MZ_COUNT_DTOR(LinuxPingSender);
if (m_socket >= 0) {
close(m_socket);
}
}
void LinuxPingSender::sendPing(const QHostAddress& dest, quint16 sequence) {
quint32 ipv4dest = dest.toIPv4Address();
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = qToBigEndian<quint32>(ipv4dest);
struct icmphdr packet;
memset(&packet, 0, sizeof(packet));
packet.type = ICMP_ECHO;
packet.un.echo.id = htons(m_ident);
packet.un.echo.sequence = htons(sequence);
packet.checksum = inetChecksum(&packet, sizeof(packet));
int rc = sendto(m_socket, &packet, sizeof(packet), 0, (struct sockaddr*)&addr,
sizeof(addr));
if (rc < 0) {
logger.error() << "failed to send:" << strerror(errno);
if (errno == ENETUNREACH) {
emit criticalPingError();
}
}
}
void LinuxPingSender::icmpSocketReady() {
socklen_t slen = 0;
unsigned char data[2048];
int rc = recvfrom(m_socket, data, sizeof(data), MSG_DONTWAIT, NULL, &slen);
if (rc <= 0) {
logger.error() << "recvfrom failed:" << strerror(errno);
return;
}
struct icmphdr packet;
if (rc >= (int)sizeof(packet)) {
memcpy(&packet, data, sizeof(packet));
if (packet.type == ICMP_ECHOREPLY) {
emit recvPing(htons(packet.un.echo.sequence));
}
}
}
void LinuxPingSender::rawSocketReady() {
socklen_t slen = 0;
unsigned char data[2048];
int rc = recvfrom(m_socket, data, sizeof(data), MSG_DONTWAIT, NULL, &slen);
if (rc <= 0) {
logger.error() << "recvfrom failed:" << strerror(errno);
return;
}
// Check the IP header
const struct iphdr* ip = (struct iphdr*)data;
int iphdrlen = ip->ihl * 4;
if (rc < iphdrlen || iphdrlen < (int)sizeof(struct iphdr)) {
logger.error() << "malformed IP packet:" << strerror(errno);
return;
}
// Check the ICMP packet
struct icmphdr packet;
if (inetChecksum(data + iphdrlen, rc - iphdrlen) != 0) {
logger.warning() << "invalid checksum";
return;
}
if (rc >= (iphdrlen + (int)sizeof(packet))) {
memcpy(&packet, data + iphdrlen, sizeof(packet));
quint16 id = htons(m_ident);
if ((packet.type == ICMP_ECHOREPLY) && (packet.un.echo.id == id)) {
emit recvPing(htons(packet.un.echo.sequence));
}
}
}
+39
View File
@@ -0,0 +1,39 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef LINUXPINGSENDER_H
#define LINUXPINGSENDER_H
#include <QObject>
#include "../../mozilla/pingsender.h"
class QSocketNotifier;
class LinuxPingSender final : public PingSender {
Q_OBJECT
Q_DISABLE_COPY_MOVE(LinuxPingSender)
public:
LinuxPingSender(const QHostAddress& source, QObject* parent = nullptr);
~LinuxPingSender();
bool isValid() override { return (m_socket >= 0); };
void sendPing(const QHostAddress& dest, quint16 sequence) override;
private:
int createSocket();
private slots:
void rawSocketReady();
void icmpSocketReady();
private:
QSocketNotifier* m_notifier = nullptr;
int m_socket = -1;
quint16 m_ident = 0;
};
#endif // LINUXPINGSENDER_H
@@ -10,8 +10,31 @@
#include "../ios/iosnetworkwatcher.h"
#include "networkwatcherimpl.h"
#include <IOKit/pwr_mgt/IOPMLib.h>
#include <IOKit/IOMessage.h>
class QString;
// Inspired by https://ladydebug.com/blog/2020/05/21/programmatically-capture-energy-saver-event-on-mac/
class PowerNotificationsListener
{
public:
PowerNotificationsListener(class MacOSNetworkWatcher* watcher) : m_watcher(watcher) {}
void registerForNotifications();
void cleanup();
private:
static void sleepWakeupCallBack(void *refParam, io_service_t service, natural_t messageType, void *messageArgument);
private:
class MacOSNetworkWatcher* m_watcher = nullptr;
IONotificationPortRef notifyPortRef = nullptr; // notification port allocated by IORegisterForSystemPower
io_object_t notifierObj = IO_OBJECT_NULL; // notifier object, used to deregister later
io_connect_t rootPowerDomain = IO_OBJECT_NULL; // a reference to the Root Power Domain IOService
};
class MacOSNetworkWatcher final : public IOSNetworkWatcher {
public:
MacOSNetworkWatcher(QObject* parent);
@@ -25,6 +48,7 @@ class MacOSNetworkWatcher final : public IOSNetworkWatcher {
private:
void* m_delegate = nullptr;
PowerNotificationsListener m_powerlistener;
};
#endif // MACOSNETWORKWATCHER_H
+242 -34
View File
@@ -6,6 +6,11 @@
#include "leakdetector.h"
#include "logger.h"
#include <QProcess>
#include <QMetaObject>
#include <pthread.h>
#include <iostream>
#import <CoreWLAN/CoreWLAN.h>
#import <Network/Network.h>
@@ -13,6 +18,37 @@ namespace {
Logger logger("MacOSNetworkWatcher");
}
// Global variables for CFRunLoop thread
static pthread_t g_powerThread;
static CFRunLoopRef g_powerRunLoop = nullptr;
static bool g_shouldStopPowerThread = false;
static PowerNotificationsListener* g_powerListener = nullptr;
// Thread function for dedicated CFRunLoop
void* powerMonitoringThread(void* arg) {
logger.debug() << "Power monitoring thread started";
PowerNotificationsListener* listener = static_cast<PowerNotificationsListener*>(arg);
// Get the runloop for this thread
g_powerRunLoop = CFRunLoopGetCurrent();
// Register for power notifications in this thread
listener->registerForNotifications();
// Run the CFRunLoop - this will block until CFRunLoopStop is called
while (!g_shouldStopPowerThread) {
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0, true);
}
// Cleanup
listener->cleanup();
g_powerRunLoop = nullptr;
logger.debug() << "Power monitoring thread finished";
return nullptr;
}
@interface MacOSNetworkWatcherDelegate : NSObject <CWEventDelegate> {
MacOSNetworkWatcher* m_watcher;
}
@@ -33,17 +69,145 @@ Logger logger("MacOSNetworkWatcher");
if (m_watcher) {
m_watcher->checkInterface();
// Emit networkChanged signal when BSSID changes
emit m_watcher->networkChanged(QString::fromNSString(interfaceName));
}
}
@end
MacOSNetworkWatcher::MacOSNetworkWatcher(QObject* parent) : IOSNetworkWatcher(parent) {
void PowerNotificationsListener::registerForNotifications()
{
logger.debug() << "Registering for system power notifications in dedicated thread";
rootPowerDomain = IORegisterForSystemPower(this, &notifyPortRef, sleepWakeupCallBack, &notifierObj);
if (rootPowerDomain == IO_OBJECT_NULL) {
logger.error() << "Failed to register for system power notifications!";
return;
}
// Add the notification port to the current runloop (dedicated thread)
CFRunLoopAddSource(CFRunLoopGetCurrent(), IONotificationPortGetRunLoopSource(notifyPortRef), kCFRunLoopCommonModes);
logger.debug() << "Power notifications registered successfully";
}
void PowerNotificationsListener::cleanup()
{
if (notifyPortRef != nullptr) {
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), IONotificationPortGetRunLoopSource(notifyPortRef), kCFRunLoopCommonModes);
IONotificationPortDestroy(notifyPortRef);
notifyPortRef = nullptr;
}
if (notifierObj != IO_OBJECT_NULL) {
IODeregisterForSystemPower(&notifierObj);
notifierObj = IO_OBJECT_NULL;
}
if (rootPowerDomain != IO_OBJECT_NULL) {
IOServiceClose(rootPowerDomain);
rootPowerDomain = IO_OBJECT_NULL;
}
}
void PowerNotificationsListener::sleepWakeupCallBack(void *refParam, io_service_t service, natural_t messageType, void *messageArgument)
{
Q_UNUSED(service)
auto listener = static_cast<PowerNotificationsListener *>(refParam);
logger.debug() << "Power callback received, messageType:" << messageType;
switch (messageType) {
case kIOMessageCanSystemSleep:
/* Idle sleep is about to kick in. This message will not be sent for forced sleep.
* Applications have a chance to prevent sleep by calling IOCancelPowerChange.
* Most applications should not prevent idle sleep. Power Management waits up to
* 30 seconds for you to either allow or deny idle sleep. If you dont acknowledge
* this power change by calling either IOAllowPowerChange or IOCancelPowerChange,
* the system will wait 30 seconds then go to sleep.
*/
logger.debug() << "System power message: can system sleep?";
// Uncomment to cancel idle sleep
// IOCancelPowerChange(thiz->rootPowerDomain, reinterpret_cast<long>(messageArgument));
// Allow idle sleep
IOAllowPowerChange(listener->rootPowerDomain, reinterpret_cast<long>(messageArgument));
break;
case kIOMessageSystemWillNotSleep:
/* Announces that the system has retracted a previous attempt to sleep; it
* follows `kIOMessageCanSystemSleep`.
*/
logger.debug() << "System power message: system will NOT sleep.";
break;
case kIOMessageSystemWillSleep:
/* The system WILL go to sleep. If you do not call IOAllowPowerChange or
* IOCancelPowerChange to acknowledge this message, sleep will be delayed by
* 30 seconds.
*
* NOTE: If you call IOCancelPowerChange to deny sleep it returns kIOReturnSuccess,
* however the system WILL still go to sleep.
*/
logger.debug() << "System power message: system WILL sleep";
IOAllowPowerChange(listener->rootPowerDomain, reinterpret_cast<long>(messageArgument));
break;
case kIOMessageSystemWillPowerOn:
/* Announces that the system is beginning to power the device tree; most devices
* are still unavailable at this point.
*/
/* From the documentation:
*
* - kIOMessageSystemWillPowerOn is delivered at early wakeup time, before most hardware
* has been powered on. Be aware that any attempts to access disk, network, the display,
* etc. may result in errors or blocking your process until those resources become
* available.
*
* So we do NOT log this event.
*/
break;
case kIOMessageSystemHasPoweredOn:
/* Announces that the system and its devices have woken up. */
logger.debug() << "System has powered on - emitting sleepMode signal from dedicated CFRunLoop thread";
if (listener->m_watcher) {
// Use QMetaObject::invokeMethod for thread-safe signal emission
QMetaObject::invokeMethod(listener->m_watcher, "sleepMode", Qt::QueuedConnection);
}
break;
default:
logger.debug() << "System power message: other event: " << messageType;
/* Not a system sleep and wake notification. */
break;
}
}
MacOSNetworkWatcher::MacOSNetworkWatcher(QObject* parent) : IOSNetworkWatcher(parent), m_powerlistener(this) {
MZ_COUNT_CTOR(MacOSNetworkWatcher);
}
MacOSNetworkWatcher::~MacOSNetworkWatcher() {
MZ_COUNT_DTOR(MacOSNetworkWatcher);
// Stop the dedicated power monitoring thread
if (g_powerListener) {
logger.debug() << "Stopping dedicated power monitoring thread";
g_shouldStopPowerThread = true;
if (g_powerRunLoop) {
CFRunLoopStop(g_powerRunLoop);
}
// Wait for thread to finish
pthread_join(g_powerThread, nullptr);
g_powerListener = nullptr;
}
if (m_delegate) {
CWWiFiClient* client = CWWiFiClient.sharedWiFiClient;
if (!client) {
@@ -66,6 +230,20 @@ void MacOSNetworkWatcher::start() {
logger.debug() << "Delegate already registered";
return;
}
// Start dedicated power monitoring thread with CFRunLoop
if (!g_powerListener) {
g_powerListener = &m_powerlistener;
g_shouldStopPowerThread = false;
int result = pthread_create(&g_powerThread, nullptr, powerMonitoringThread, &m_powerlistener);
if (result != 0) {
logger.error() << "Failed to create power monitoring thread:" << result;
g_powerListener = nullptr;
} else {
logger.debug() << "Power monitoring enabled";
}
}
CWWiFiClient* client = CWWiFiClient.sharedWiFiClient;
if (!client) {
@@ -77,6 +255,8 @@ void MacOSNetworkWatcher::start() {
m_delegate = [[MacOSNetworkWatcherDelegate alloc] initWithObject:this];
[client setDelegate:static_cast<MacOSNetworkWatcherDelegate*>(m_delegate)];
[client startMonitoringEventWithType:CWEventTypeBSSIDDidChange error:nullptr];
logger.debug() << "MacOSNetworkWatcher started successfully";
}
void MacOSNetworkWatcher::checkInterface() {
@@ -87,42 +267,70 @@ void MacOSNetworkWatcher::checkInterface() {
return;
}
CWWiFiClient* client = CWWiFiClient.sharedWiFiClient;
if (!client) {
logger.debug() << "Unable to retrieve the CWWiFiClient shared instance";
// Use wdutil to get reliable WiFi information
QProcess process;
process.start("wdutil", QStringList() << "info");
process.waitForFinished(5000);
QString output = process.readAllStandardOutput();
QString errorOutput = process.readAllStandardError();
logger.debug() << "wdutil exit code:" << process.exitCode();
if (process.exitCode() != 0) {
logger.debug() << "wdutil failed with exit code:" << process.exitCode();
return;
}
CWInterface* interface = [client interface];
if (!interface) {
logger.debug() << "No default wifi interface";
return;
// Parse wdutil output to find WiFi connection info
QStringList lines = output.split('\n');
QString ssid, interfaceName, security;
bool wifiSectionFound = false;
for (int i = 0; i < lines.size(); i++) {
QString trimmedLine = lines[i].trimmed();
if (trimmedLine == "WIFI") {
wifiSectionFound = true;
continue;
}
if (wifiSectionFound) {
// Stop parsing when we reach next section header (all caps after separator line)
if (trimmedLine.startsWith("————————")) {
if (i + 1 < lines.size()) {
QString nextLine = lines[i + 1].trimmed();
if (!nextLine.isEmpty() && nextLine.length() > 2 && nextLine.toUpper() == nextLine && nextLine != "WIFI") {
break;
}
}
continue; // Skip separator lines
}
if (trimmedLine.startsWith("Interface Name")) {
QStringList parts = trimmedLine.split(":");
if (parts.size() >= 2) {
interfaceName = parts[1].trimmed();
}
} else if (trimmedLine.startsWith("SSID")) {
QStringList parts = trimmedLine.split(":");
if (parts.size() >= 2) {
ssid = parts[1].trimmed();
}
} else if (trimmedLine.startsWith("Security")) {
QStringList parts = trimmedLine.split(":");
if (parts.size() >= 2) {
security = parts[1].trimmed();
}
}
}
}
if (![interface powerOn]) {
logger.debug() << "The interface is off";
return;
if (!ssid.isEmpty() && !interfaceName.isEmpty()) {
logger.debug() << "Found active WiFi connection on" << interfaceName
<< "SSID:" << ssid << "Security:" << security;
} else {
logger.debug() << "No active WiFi connection found";
}
NSString* ssidNS = [interface ssid];
if (!ssidNS) {
logger.debug() << "WiFi is not in used";
return;
}
QString ssid = QString::fromNSString(ssidNS);
if (ssid.isEmpty()) {
logger.debug() << "WiFi doesn't have a valid SSID";
return;
}
CWSecurity security = [interface security];
if (security == kCWSecurityNone || security == kCWSecurityWEP) {
logger.debug() << "Unsecured network found!";
emit unsecuredNetwork(ssid, ssid);
return;
}
logger.debug() << "Secure WiFi interface";
}
@@ -22,7 +22,6 @@
#include "logger.h"
#include "platforms/windows/daemon/windowsfirewall.h"
#include "platforms/windows/daemon/windowssplittunnel.h"
#include "platforms/windows/windowscommons.h"
#include "windowsfirewall.h"
#include "core/networkUtilities.h"
@@ -32,9 +32,28 @@ WindowsNetworkWatcher::~WindowsNetworkWatcher() {
}
}
LRESULT WindowsNetworkWatcher::PowerWndProcCallback(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
auto obj = reinterpret_cast<WindowsNetworkWatcher*>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
if (!obj){
logger.debug() << "obj not casted";
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
switch (uMsg) {
case WM_POWERBROADCAST:
if (wParam == PBT_APMRESUMESUSPEND) {
emit obj->sleepMode();
}
break;
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
return 0;
}
void WindowsNetworkWatcher::initialize() {
logger.debug() << "initialize";
DWORD negotiatedVersion;
if (WlanOpenHandle(2, nullptr, &negotiatedVersion, &m_wlanHandle) !=
ERROR_SUCCESS) {
@@ -51,6 +70,25 @@ void WindowsNetworkWatcher::initialize() {
return;
}
const wchar_t* className = L"PowerMonitorClass";
WNDCLASS wc = { 0 };
wc.lpfnWndProc = &WindowsNetworkWatcher::PowerWndProcCallback;
wc.hInstance = GetModuleHandle(NULL);
wc.lpszClassName = className;
wc.cbWndExtra = sizeof(WindowsNetworkWatcher*);
if (!RegisterClass(&wc)) {
logger.debug() << "Failed to register window class in createPowerMonitorWindow.";
return;
}
HWND hwnd = CreateWindowEx(0, className, L"Power Monitor", 0, 0, 0, 0, 0, NULL, NULL, GetModuleHandle(NULL), static_cast<LPVOID>(this));
if (!hwnd) {
logger.debug() << "Failed to create window in createPowerMonitorWindow.";
return;
}
SetWindowLongPtr(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(this));
logger.debug() << "callback registered";
}
@@ -137,4 +175,4 @@ void WindowsNetworkWatcher::processWlan(PWLAN_NOTIFICATION_DATA data) {
logger.debug() << "Unsecure network:" << logger.sensitive(ssid)
<< "id:" << logger.sensitive(bssid);
emit unsecuredNetwork(ssid, bssid);
}
}
@@ -19,6 +19,7 @@ class WindowsNetworkWatcher final : public NetworkWatcherImpl {
private:
static void wlanCallback(PWLAN_NOTIFICATION_DATA data, PVOID context);
static LRESULT PowerWndProcCallback(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
void processWlan(PWLAN_NOTIFICATION_DATA data);
@@ -179,6 +179,7 @@ void WindowsPingSender::pingEventReady() {
return;
}
QString errmsg = WindowsUtils::getErrorMessage();
emit criticalPingError();
logger.error() << "No ping reply. Code: " << error
<< " Message: " << errmsg;
return;
+2 -1
View File
@@ -56,7 +56,8 @@ void OpenVpnProtocol::stop()
}
#if defined(Q_OS_WIN) || defined(Q_OS_LINUX) || defined(Q_OS_MACOS)
IpcClient::Interface()->disableKillSwitch();
QRemoteObjectPendingReply<bool> disableKillSwitchResp = IpcClient::Interface()->disableKillSwitch();
disableKillSwitchResp.waitForFinished(1000);
#endif
setConnectionState(Vpn::ConnectionState::Disconnected);
+5
View File
@@ -103,6 +103,11 @@ QString VpnProtocol::vpnGateway() const
return m_vpnGateway;
}
QString VpnProtocol::vpnLocalAddress() const
{
return m_vpnLocalAddress;
}
VpnProtocol *VpnProtocol::factory(DockerContainer container, const QJsonObject &configuration)
{
switch (container) {
+2
View File
@@ -63,6 +63,7 @@ public:
QString routeGateway() const;
QString vpnGateway() const;
QString vpnLocalAddress() const;
static VpnProtocol* factory(amnezia::DockerContainer container, const QJsonObject &configuration);
@@ -71,6 +72,7 @@ signals:
void connectionStateChanged(Vpn::ConnectionState state);
void timeoutTimerEvent();
void protocolError(amnezia::ErrorCode e);
void tunnelAddressesUpdated(const QString& gateway, const QString& localAddress);
public slots:
virtual void onTimeout(); // todo: remove?
+20 -1
View File
@@ -17,6 +17,26 @@ WireguardProtocol::WireguardProtocol(const QJsonObject &configuration, QObject *
[this](const QString &pubkey, const QDateTime &connectionTimestamp) {
emit connectionStateChanged(Vpn::ConnectionState::Connected);
});
connect(m_impl.get(), &ControllerImpl::statusUpdated, this,
[this](const QString& serverIpv4Gateway,
const QString& deviceIpv4Address, uint64_t txBytes,
uint64_t rxBytes) {
const QString previousGateway = m_vpnGateway;
const QString previousLocal = m_vpnLocalAddress;
if (!serverIpv4Gateway.isEmpty()) {
m_vpnGateway = serverIpv4Gateway;
}
if (!deviceIpv4Address.isEmpty()) {
m_vpnLocalAddress = deviceIpv4Address;
}
if ((!m_vpnGateway.isEmpty() && m_vpnGateway != previousGateway) ||
(!m_vpnLocalAddress.isEmpty() && m_vpnLocalAddress != previousLocal)) {
emit tunnelAddressesUpdated(m_vpnGateway, m_vpnLocalAddress);
}
});
connect(m_impl.get(), &ControllerImpl::disconnected, this,
[this]() { emit connectionStateChanged(Vpn::ConnectionState::Disconnected); });
m_impl->initialize(nullptr, nullptr);
@@ -57,4 +77,3 @@ ErrorCode WireguardProtocol::start()
{
return startMzImpl();
}
+12 -3
View File
@@ -166,9 +166,17 @@ ErrorCode XrayProtocol::startTun2Sock()
void XrayProtocol::stop()
{
#if defined(Q_OS_WIN) || defined(Q_OS_LINUX) || defined(Q_OS_MACOS)
IpcClient::Interface()->disableKillSwitch();
IpcClient::Interface()->StartRoutingIpv6();
#ifdef AMNEZIA_DESKTOP
QRemoteObjectPendingReply<bool> disableKillSwitchResp = IpcClient::Interface()->disableKillSwitch();
disableKillSwitchResp.waitForFinished(1000);
QRemoteObjectPendingReply<bool> StartRoutingIpv6Resp = IpcClient::Interface()->StartRoutingIpv6();
StartRoutingIpv6Resp.waitForFinished(1000);
QRemoteObjectPendingReply<bool> restoreResolvers = IpcClient::Interface()->restoreResolvers();
restoreResolvers.waitForFinished(1000);
#if !defined(Q_OS_MACOS)
QRemoteObjectPendingReply<bool> deleteTunResp = IpcClient::Interface()->deleteTun("tun2");
deleteTunResp.waitForFinished(1000);
#endif
#endif
qDebug() << "XrayProtocol::stop()";
m_xrayProcess.disconnect();
@@ -176,6 +184,7 @@ void XrayProtocol::stop()
m_xrayProcess.waitForFinished(3000);
if (m_t2sProcess) {
m_t2sProcess->stop();
QThread::msleep(200);
}
setConnectionState(Vpn::ConnectionState::Disconnected);
+7 -1
View File
@@ -35,6 +35,9 @@
<file>images/controls/mail.svg</file>
<file>images/controls/map-pin.svg</file>
<file>images/controls/more-vertical.svg</file>
<file>images/controls/news.svg</file>
<file>images/controls/news-unread.svg</file>
<file>images/controls/unread-dot.svg</file>
<file>images/controls/plus.svg</file>
<file>images/controls/qr-code.svg</file>
<file>images/controls/radio-button-inner-circle-pressed.png</file>
@@ -49,6 +52,7 @@
<file>images/controls/server.svg</file>
<file>images/controls/settings-2.svg</file>
<file>images/controls/settings.svg</file>
<file>images/controls/settings-news.svg</file>
<file>images/controls/share-2.svg</file>
<file>images/controls/split-tunneling.svg</file>
<file>images/controls/tag.svg</file>
@@ -127,7 +131,6 @@
<file>ui/qml/Components/SelectLanguageDrawer.qml</file>
<file>ui/qml/Components/ServersListView.qml</file>
<file>ui/qml/Components/SettingsContainersListView.qml</file>
<file>ui/qml/Components/TransportProtoSelector.qml</file>
<file>ui/qml/Components/AddSitePanel.qml</file>
<file>ui/qml/Config/GlobalConfig.qml</file>
@@ -212,6 +215,8 @@
<file>ui/qml/Pages2/PageSettingsServerServices.qml</file>
<file>ui/qml/Pages2/PageSettingsServersList.qml</file>
<file>ui/qml/Pages2/PageSettingsSplitTunneling.qml</file>
<file>ui/qml/Pages2/PageSettingsNewsNotifications.qml</file>
<file>ui/qml/Pages2/PageSettingsNewsDetail.qml</file>
<file>ui/qml/Pages2/PageProtocolAwgClientSettings.qml</file>
<file>ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml</file>
<file>ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml</file>
@@ -242,6 +247,7 @@
<file>ui/qml/Components/OtpCodeDrawer.qml</file>
<file>ui/qml/Components/AwgTextField.qml</file>
<file>ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml</file>
<file>ui/qml/Components/SmartScroll.qml</file>
</qresource>
<qresource prefix="/countriesFlags">
<file>images/flagKit/ZW.svg</file>
+519
View File
@@ -0,0 +1,519 @@
#!/bin/sh
LOG_DATE=$(date -u +'%Y%m%d-%H%M%S')
SCRIPT_DIR=$(dirname "$0")
LOG_FILE="${SCRIPT_DIR}/server-diagnostics-${LOG_DATE}.log"
# Logging function (sh compatible)
log_and_display() {
if [ "$1" = "-n" ]; then
shift
printf "%s" "$*" | tee -a "$LOG_FILE"
else
echo "$1" | tee -a "$LOG_FILE"
fi
}
# Redirect stderr to stdout for logging
exec 2>&1
header() {
log_and_display ""
log_and_display "=== $1 ==="
}
# Pause for cancellation
log_and_display ""
log_and_display "VPN Server Diagnostics will start in 9s. Press Ctrl+C to cancel."
sleep 9
log_and_display ""
header "STARTING VPN SERVER DIAGNOSTICS"
log_and_display ""
# ------------------------------------------------------------------------------
# 1. Basic system information
# ------------------------------------------------------------------------------
header "System Information"
# Uptime
UPTIME_STR=$(awk '{printf "%d:%02d:%02d", int($1/3600), int(($1%3600)/60), int($1%60)}' /proc/uptime 2>/dev/null || echo "unknown")
log_and_display "Uptime (H:M:S): $UPTIME_STR"
# Date/time UTC
DATE_UTC=$(date -u +'%d %b %Y|%T' 2>/dev/null || echo "unknown")
log_and_display "Date|Time (UTC): $DATE_UTC"
# Init system (PID 1)
INIT_NAME=$(cat /proc/1/status 2>/dev/null | head -1 | awk '{print $2}' 2>/dev/null || echo "unknown")
log_and_display "Init system (PID 1): $INIT_NAME"
# Locale
if echo "$LANG" | grep -E '^(en_US.UTF-8|C.UTF-8|C)$' >/dev/null 2>&1; then
log_and_display "Locale: $LANG"
else
log_and_display "Locale: $LANG (not en_US.UTF-8, C.UTF-8 or C)"
fi
# ------------------------------------------------------------------------------
# 2. Package manager detection
# ------------------------------------------------------------------------------
header "Package Manager Information"
if command -v apt-get >/dev/null 2>&1; then
log_and_display "Package Manager: APT"
PM="apt-get"
PM_VER_OPT="--version"
DOCKER_PKG="docker.io"
elif command -v dnf >/dev/null 2>&1; then
log_and_display "Package Manager: DNF"
PM="dnf"
PM_VER_OPT="--version"
DOCKER_PKG="docker"
elif command -v yum >/dev/null 2>&1; then
log_and_display "Package Manager: YUM"
PM="yum"
PM_VER_OPT="--version"
DOCKER_PKG="docker"
elif command -v zypper >/dev/null 2>&1; then
log_and_display "Package Manager: ZYPPER"
PM="zypper"
PM_VER_OPT="--version"
DOCKER_PKG="docker"
elif command -v pacman >/dev/null 2>&1; then
log_and_display "Package Manager: PACMAN"
PM="pacman"
PM_VER_OPT="--version"
DOCKER_PKG="docker"
elif command -v opkg >/dev/null 2>&1; then
log_and_display "Package Manager: OPKG - Not supported on this platform"
PM="opkg"
PM_VER_OPT="--version"
DOCKER_PKG="docker"
else
log_and_display "Package Manager: Unknown"
# fallback
PM="uname"
PM_VER_OPT="-a"
DOCKER_PKG="docker"
fi
# Check package versions
log_and_display ""
log_and_display "Package versions:"
# Check sudo
if [ "$PM" = "apt-get" ]; then
sudo_version=$(dpkg -s "sudo" 2>/dev/null | grep '^Version:' | awk '{print $2}' || echo "not installed")
elif [ "$PM" = "dnf" ] || [ "$PM" = "yum" ] || [ "$PM" = "zypper" ]; then
sudo_version=$(rpm -q "sudo" 2>/dev/null || echo "not installed")
elif [ "$PM" = "pacman" ]; then
sudo_version=$(pacman -Q "sudo" 2>/dev/null || echo "not installed")
elif [ "$PM" = "opkg" ]; then
sudo_version=$(opkg info "sudo" 2>/dev/null | grep '^Version:' | awk '{print $2}' || echo "not installed")
else
sudo_version="unknown"
fi
log_and_display " sudo: $sudo_version"
# Check Docker package
if [ "$PM" = "apt-get" ]; then
docker_pkg_version=$(dpkg -s "$DOCKER_PKG" 2>/dev/null | grep '^Version:' | awk '{print $2}' || echo "not installed")
elif [ "$PM" = "dnf" ] || [ "$PM" = "yum" ] || [ "$PM" = "zypper" ]; then
docker_pkg_version=$(rpm -q "$DOCKER_PKG" 2>/dev/null || echo "not installed")
elif [ "$PM" = "pacman" ]; then
docker_pkg_version=$(pacman -Q "$DOCKER_PKG" 2>/dev/null || echo "not installed")
elif [ "$PM" = "opkg" ]; then
docker_pkg_version=$(opkg info "$DOCKER_PKG" 2>/dev/null | grep '^Version:' | awk '{print $2}' || echo "not installed")
else
docker_pkg_version="unknown"
fi
log_and_display " $DOCKER_PKG: $docker_pkg_version"
# Check lsof
if [ "$PM" = "apt-get" ]; then
lsof_version=$(dpkg -s "lsof" 2>/dev/null | grep '^Version:' | awk '{print $2}' || echo "not installed")
elif [ "$PM" = "dnf" ] || [ "$PM" = "yum" ] || [ "$PM" = "zypper" ]; then
lsof_version=$(rpm -q "lsof" 2>/dev/null || echo "not installed")
elif [ "$PM" = "pacman" ]; then
lsof_version=$(pacman -Q "lsof" 2>/dev/null || echo "not installed")
elif [ "$PM" = "opkg" ]; then
lsof_version=$(opkg info "lsof" 2>/dev/null | grep '^Version:' | awk '{print $2}' || echo "not installed")
else
lsof_version="unknown"
fi
log_and_display " lsof: $lsof_version"
# ------------------------------------------------------------------------------
# 3. Additional system information (hostnamectl / /proc/version)
# ------------------------------------------------------------------------------
header "OS / Kernel Information"
if command -v hostnamectl >/dev/null 2>&1; then
hostnamectl 2>/dev/null | grep -E 'Operating System:|Virtualization:|Kernel:|Architecture:' | sed 's/^[ \t]*//;s/:/: /' | while read line; do
log_and_display " $line"
done
else
log_and_display "Operating System: $(cat /proc/version 2>/dev/null || echo 'unknown')"
fi
# CPU threads
CPU_THREADS=$(nproc 2>/dev/null || grep -c "^processor" /proc/cpuinfo 2>/dev/null || echo "unknown")
log_and_display " CPU threads: $CPU_THREADS"
# ------------------------------------------------------------------------------
# 4. Memory (RAM) check
# ------------------------------------------------------------------------------
header "Memory Information"
if command -v free >/dev/null 2>&1; then
# Remove extra spaces in header
free -h 2>/dev/null | tee -a "$LOG_FILE" || log_and_display " Error getting memory info"
elif command -v vmstat >/dev/null 2>&1; then
vmstat -S M -s 2>/dev/null | grep -iE 'total memory|total swap' | sed 's/ *//' | tee -a "$LOG_FILE" || log_and_display " Error getting memory info"
else
grep -iE 'MemTotal|SwapTotal' /proc/meminfo 2>/dev/null | sed 's/ \+/ /' | tee -a "$LOG_FILE" || log_and_display " Error getting memory info"
fi
if command -v free >/dev/null 2>&1; then
log_and_display ""
log_and_display "Detailed Memory Info:"
free -h 2>/dev/null | awk 'NR==2{printf " Used: %s / %s (%.1f%%)\n", $3, $2, $3/$2*100}' 2>/dev/null | tee -a "$LOG_FILE" || log_and_display " Error calculating memory usage"
free -h 2>/dev/null | awk 'NR==3{printf " Swap: %s / %s (%.1f%%)\n", $3, $2, $2>0 ? $3/$2*100 : 0}' 2>/dev/null | tee -a "$LOG_FILE" || log_and_display " Error calculating swap usage"
fi
# Disk usage
header "Disk Usage"
df -h 2>/dev/null | awk '
BEGIN {print " Filesystem Size Used Avail Use% Mounted"}
NR>1 {printf " %-10s %5s %5s %5s %4s %s\n", $1, $2, $3, $4, $5, $6}' | tee -a "$LOG_FILE" || log_and_display " Error getting disk usage"
# ------------------------------------------------------------------------------
# 5. Current user and sudo check
# ------------------------------------------------------------------------------
header "User Check"
CUR_USER=$(whoami 2>/dev/null || echo ~ | sed 's/.*\///')
USER_GROUP=$(groups "$CUR_USER" 2>/dev/null || echo "")
USER_GOOD=0
log_and_display -n "Current user: $CUR_USER => "
if [ "$CUR_USER" = "root" ]; then
log_and_display "passed.. (is root)"
USER_GOOD="r" # root
else
if echo "$USER_GROUP" | grep -qE '(^|[[:space:]])sudo($|[[:space:]])'; then
log_and_display "passed.. (in sudo group)"
USER_GOOD=1
elif echo "$USER_GROUP" | grep -qE '(^|[[:space:]])wheel($|[[:space:]])'; then
log_and_display "passed.. (in wheel group)"
USER_GOOD=1
elif echo "$USER_GROUP" | grep -qE '(^|[[:space:]])docker($|[[:space:]])'; then
log_and_display "failed.. (only in docker group)"
USER_GOOD="d"
else
log_and_display "failed.. (not a member of the sudo or wheel groups)"
USER_GOOD=0
fi
fi
# Check if password is required for sudo
if [ "$USER_GOOD" = "0" ] || [ "$USER_GOOD" = "d" ]; then
log_and_display -n "Passwd request: "
log_and_display "check skipped (not sudoer)"
else
if command -v sudo >/dev/null 2>&1; then
# Try sudo without password - more thorough check
PASSWD_REQUEST=$(sudo -K 2>&1 && sudo -nu $CUR_USER $PM $PM_VER_OPT 2>&1 >/dev/null && sudo -n $PM $PM_VER_OPT 2>&1 >/dev/null)
if [ -n "$PASSWD_REQUEST" ]; then
USER_GOOD=0
log_and_display -n "Passwd request: "
log_and_display "failed.. ($PASSWD_REQUEST)" \
| sed "s/$CUR_USER/User/g;s/$(hostname 2>/dev/null || echo 'Server')/Server/g;s/ user / /g"
else
log_and_display -n "Passwd request: "
log_and_display "passed.. (not required)"
fi
else
if [ "$USER_GOOD" = "r" ]; then
log_and_display -n "Passwd request: "
log_and_display "check skipped (sudo not installed, but root user)"
else
log_and_display "Warning! The sudo package must be pre-installed!"
USER_GOOD=0
fi
fi
fi
# Home directory check
log_and_display -n "Home dir: "
if cd ~ 2>/dev/null; then
log_and_display "passed.. (accessible)"
else
log_and_display "failed.. (not accessible)"
fi
log_and_display "Default shell: $SHELL"
# ------------------------------------------------------------------------------
# 6. Important components check (sudo, lsof, fuser, apparmor)
# ------------------------------------------------------------------------------
header "Component Checks"
log_and_display -n " sudo: "
if command -v sudo >/dev/null 2>&1; then
log_and_display "passed.. (installed)"
else
log_and_display "not installed"
fi
log_and_display -n " lsof: "
if command -v lsof >/dev/null 2>&1; then
log_and_display "passed.. (installed)"
else
log_and_display "not installed"
fi
log_and_display -n " fuser: "
if command -v fuser >/dev/null 2>&1; then
log_and_display "passed.. (installed)"
else
log_and_display "psmisc not installed"
fi
log_and_display -n "apparmor: "
AA_ENABLED=$(cat /sys/module/apparmor/parameters/enabled 2>/dev/null || echo "N")
if [ "$AA_ENABLED" = "Y" ]; then
if command -v apparmor_parser >/dev/null 2>&1; then
log_and_display "passed.. (used)"
else
log_and_display "failed.. (installation required)"
fi
else
if command -v apparmor_parser >/dev/null 2>&1; then
log_and_display "passed.. (not used)"
else
log_and_display "passed.. (not required)"
fi
fi
# ------------------------------------------------------------------------------
# 7. SELinux check
# ------------------------------------------------------------------------------
header "SELinux Check"
if command -v getenforce >/dev/null 2>&1; then
SELINUX_STATUS=$(getenforce 2>/dev/null || echo "unknown")
if [ "$SELINUX_STATUS" = "Enforcing" ]; then
log_and_display "SELinux status: $SELINUX_STATUS (strict mode)"
elif [ "$SELINUX_STATUS" = "Permissive" ]; then
log_and_display "SELinux status: $SELINUX_STATUS (permissive mode)"
else
log_and_display "SELinux status: $SELINUX_STATUS (disabled)"
fi
else
log_and_display "SELinux: not found (or not applicable)"
fi
# ------------------------------------------------------------------------------
# 8. Docker + Docker/Podman service check
# ------------------------------------------------------------------------------
header "Docker / Podman Status"
CHECK_CONTAINERS=0
if ! command -v docker >/dev/null 2>&1; then
log_and_display "Docker: $DOCKER_PKG not installed"
else
# If user is in sudoers, use sudo without password
if [ "$USER_GOOD" = "1" ]; then
SUD="sudo -n"
elif [ "$USER_GOOD" = "r" ]; then
SUD="" # root
else
SUD=""
fi
DOCKER_VERSION=$($SUD docker -v 2>/dev/null || echo 'docker -v error')
log_and_display "Installed: $DOCKER_VERSION"
# Check for podman
if echo "$DOCKER_VERSION" | grep -qi "podman"; then
log_and_display " WARNING: Podman detected - not supported at the moment!"
log_and_display " Podman (podman-docker) is not supported and is installed by mistake"
docker_service="podman.socket"
else
docker_service="docker.service"
fi
log_and_display " service: $docker_service"
# Check status
if command -v systemctl >/dev/null 2>&1; then
docker_status=$(systemctl is-active "$docker_service" 2>/dev/null || echo "unknown")
docker_loading=$(systemctl is-enabled "$docker_service" 2>/dev/null || echo "unknown")
else
docker_status="unknown (systemctl not found)"
docker_loading="unknown"
fi
if [ "$docker_status" = "active" ]; then
log_and_display " status: passed.. ($docker_status)"
CHECK_CONTAINERS=1
else
log_and_display " status: incorrect.. ($docker_status)"
CHECK_CONTAINERS=0
fi
if [ "$docker_loading" = "enabled" ]; then
log_and_display " loading: good (startup $docker_loading)"
else
log_and_display " loading: bad (startup $docker_loading)"
fi
fi
# ------------------------------------------------------------------------------
# 9. Docker pull test + container check with improved Docker Hub verification
# ------------------------------------------------------------------------------
header "Docker Hub: pull hello-world test"
if [ "$CHECK_CONTAINERS" = "1" ] && [ "$USER_GOOD" != "0" ]; then
# First check Docker Hub availability
log_and_display "Checking Docker Hub connectivity..."
# Try to execute docker pull with timeout
if timeout 30 $SUD docker pull docker.io/library/hello-world >/dev/null 2>&1; then
log_and_display "Docker Hub: available"
# Start container for testing
if $SUD docker run --rm docker.io/library/hello-world >/dev/null 2>&1; then
log_and_display "Hello-world container: successfully started and completed"
else
log_and_display "Hello-world container: startup error"
fi
else
log_and_display "Docker Hub: unavailable or blocked (possibly exceeded download limit)"
log_and_display "Docker Hub has download limits, try again later"
fi
log_and_display ""
total_cont=$($SUD docker ps -aq 2>/dev/null | wc -l || echo "0")
active_cont=$($SUD docker ps -q 2>/dev/null | wc -l || echo "0")
amnezia_cont=$($SUD docker ps -a 2>/dev/null | grep -c amnezia || echo "0")
log_and_display "Containers check: Total $total_cont / Active $active_cont / Amnezia $amnezia_cont"
$SUD docker ps -a --format "{{.Names}} ({{.Image}}) ({{.Status}}) ({{.Ports}})" 2>/dev/null | grep amnezia || true
# Peers check
if $SUD docker ps 2>/dev/null | grep -qE '\<(amnezia-awg|amnezia-wireguard)\>'; then
log_and_display ""
log_and_display "Peers check (beta):"
if $SUD docker ps 2>/dev/null | grep -q amnezia-awg; then
AMNEZIA_WG_CONTAINER=$($SUD docker ps 2>/dev/null | grep amnezia-awg | awk '{print $1}' | head -1)
if [ -n "$AMNEZIA_WG_CONTAINER" ]; then
WG_PEERS=$($SUD docker exec -it "$AMNEZIA_WG_CONTAINER" wg show 2>/dev/null | grep -c 'peer' || echo "0")
log_and_display "AmneziaWG peers: $WG_PEERS"
fi
fi
if $SUD docker ps 2>/dev/null | grep -q amnezia-wireguard; then
WIREGUARD_CONTAINER=$($SUD docker ps 2>/dev/null | grep amnezia-wireguard | awk '{print $1}' | head -1)
if [ -n "$WIREGUARD_CONTAINER" ]; then
WG_PEERS=$($SUD docker exec -it "$WIREGUARD_CONTAINER" wg show 2>/dev/null | grep -c 'peer' || echo "0")
log_and_display "WireGuard peers: $WG_PEERS"
fi
fi
fi
else
log_and_display "skipped.."
fi
# ------------------------------------------------------------------------------
# 10. Additional improvements
# ------------------------------------------------------------------------------
#
# 10.1. CPU and memory load check (Load average, top processes)
#
header "CPU & Memory usage (top)"
# Load average (last 1,5,15 minutes)
LOAD_AVG=$(uptime 2>/dev/null | awk -F'load average:' '{print $2}' || echo "unknown")
log_and_display "Load average: $LOAD_AVG"
log_and_display ""
log_and_display "Top 5 processes by CPU:"
ps aux 2>/dev/null | sort -k3 -nr | head -n 6 | awk '{printf "%s %s %s %s %s\n", $1,$2,$3"%",$4"%",$11}' | column -t 2>/dev/null | tee -a "$LOG_FILE" || log_and_display " Error getting CPU processes"
log_and_display ""
log_and_display "Top 5 processes by MEM:"
ps aux 2>/dev/null | sort -k4 -nr | head -n 6 | awk '{printf "%s %s %s %s %s\n", $1,$2,$3"%",$4"%",$11}' | column -t 2>/dev/null | tee -a "$LOG_FILE" || log_and_display " Error getting MEM processes"
# 10.2. System logs check (latest critical messages)
header "Last 10 critical/error messages (journalctl)"
if command -v journalctl >/dev/null 2>&1; then
journalctl -p 3 -n 10 --no-pager 2>/dev/null | tee -a "$LOG_FILE" || log_and_display " Error getting system logs"
else
log_and_display "journalctl not found (non-systemd system?)"
fi
# 10.3. System package versions check (examples)
# Open ports check
header "Network Ports Check"
if command -v netstat >/dev/null 2>&1; then
log_and_display "Listening ports:"
netstat -tlnp 2>/dev/null | grep LISTEN | head -10 | while read line; do
log_and_display " $line"
done
elif command -v ss >/dev/null 2>&1; then
log_and_display "Listening ports:"
ss -tlnp 2>/dev/null | head -10 | while read line; do
log_and_display " $line"
done
else
log_and_display "netstat/ss not found"
fi
# SSH check
header "SSH Service Check"
if command -v systemctl >/dev/null 2>&1; then
ssh_status=$(systemctl is-active ssh 2>/dev/null || systemctl is-active sshd 2>/dev/null || echo "not found")
if [ "$ssh_status" = "active" ]; then
log_and_display "SSH service: $ssh_status"
else
log_and_display "SSH service: $ssh_status"
fi
else
log_and_display "systemctl not found"
fi
# Time check
header "Time Synchronization"
if command -v timedatectl >/dev/null 2>&1; then
timedatectl status 2>/dev/null | grep -E "System clock|NTP service" | while read line; do
log_and_display " $line"
done
else
log_and_display " System time: $(date 2>/dev/null || echo 'unknown')"
fi
# Kernel check
header "Kernel Information"
log_and_display "Kernel version: $(uname -r 2>/dev/null || echo 'unknown')"
log_and_display "Kernel architecture: $(uname -m 2>/dev/null || echo 'unknown')"
if [ -f /proc/cmdline ]; then
log_and_display "Kernel parameters:"
cat /proc/cmdline 2>/dev/null | tr ' ' '\n' | head -5 | while read param; do
log_and_display " $param"
done
fi
# ------------------------------------------------------------------------------
# Completion
# ------------------------------------------------------------------------------
log_and_display ""
header "FINISH"
log_and_display ""
log_and_display "Diagnostics completed. Log saved to: $LOG_FILE"
log_and_display ""
# Variable cleanup
pm="" && opt="" && docker_pkg="" && CUR_USER="" && USER_GOOD="" && USER_GROUP="" && PASSWD_REQUEST="" && CHECK_CONTAINERS="" && SUD="" && docker_service="" && docker_status="" && docker_loading=""
+12 -2
View File
@@ -541,12 +541,12 @@ QString Settings::getGatewayEndpoint()
bool Settings::isDevGatewayEnv()
{
return m_isDevGatewayEnv;
return value("Conf/devGatewayEnv", false).toBool();
}
void Settings::toggleDevGatewayEnv(bool enabled)
{
m_isDevGatewayEnv = enabled;
setValue("Conf/devGatewayEnv", enabled);
}
bool Settings::isHomeAdLabelVisible()
@@ -578,3 +578,13 @@ void Settings::setAllowedDnsServers(const QStringList &servers)
{
setValue("Conf/allowedDnsServers", servers);
}
QStringList Settings::readNewsIds() const
{
return value("News/readIds").toStringList();
}
void Settings::setReadNewsIds(const QStringList &ids)
{
setValue("News/readIds", ids);
}
+4 -2
View File
@@ -174,7 +174,7 @@ public:
QLocale getAppLanguage()
{
QString localeStr = m_settings.value("Conf/appLanguage").toString();
QString localeStr = m_settings.value("Conf/appLanguage", QLocale::system().name()).toString();
return QLocale(localeStr);
};
void setAppLanguage(QLocale locale)
@@ -236,6 +236,9 @@ public:
QStringList allowedDnsServers() const;
void setAllowedDnsServers(const QStringList &servers);
QStringList readNewsIds() const;
void setReadNewsIds(const QStringList &ids);
signals:
void saveLogsChanged(bool enabled);
void screenshotsEnabledChanged(bool enabled);
@@ -251,7 +254,6 @@ private:
mutable SecureQSettings m_settings;
QString m_gatewayEndpoint;
bool m_isDevGatewayEnv = false;
};
#endif // SETTINGS_H
File diff suppressed because it is too large Load Diff
+144 -136
View File
@@ -4,9 +4,8 @@
<context>
<name>AdLabel</name>
<message>
<location filename="../ui/qml/Components/AdLabel.qml" line="57"/>
<source>Amnezia Premium - for access to all websites and online resources</source>
<translation>Amnezia Premium - доступ ко всем сайтам и онлайн ресурсам</translation>
<translation type="vanished">Amnezia Premium - доступ ко всем сайтам и онлайн ресурсам</translation>
</message>
</context>
<context>
@@ -61,7 +60,7 @@
<name>ApiAccountInfoModel</name>
<message>
<location filename="../ui/models/api/apiAccountInfoModel.cpp" line="31"/>
<location filename="../ui/models/api/apiAccountInfoModel.cpp" line="34"/>
<location filename="../ui/models/api/apiAccountInfoModel.cpp" line="35"/>
<source>Active</source>
<translation>Активна</translation>
</message>
@@ -71,35 +70,33 @@
<translation>Не активна</translation>
</message>
<message>
<location filename="../ui/models/api/apiAccountInfoModel.cpp" line="47"/>
<location filename="../ui/models/api/apiAccountInfoModel.cpp" line="48"/>
<source>%1 out of %2</source>
<translation>%1 из %2</translation>
</message>
<message>
<location filename="../ui/models/api/apiAccountInfoModel.cpp" line="51"/>
<source>Classic VPN for seamless work, downloading large files, and watching videos. Access all websites and online resources. Speeds up to 200 Mbps</source>
<translation>Классический VPN для комфортной работы, загрузки больших файлов и просмотра видео. Доступ ко всем сайтам и онлайн-ресурсам. Скорость до 200 Мбит/с</translation>
<translation type="vanished">Классический VPN для комфортной работы, загрузки больших файлов и просмотра видео. Доступ ко всем сайтам и онлайн-ресурсам. Скорость до 200 Мбит/с</translation>
</message>
<message>
<location filename="../ui/models/api/apiAccountInfoModel.cpp" line="55"/>
<source>Free unlimited access to a basic set of websites such as Facebook, Instagram, Twitter (X), Discord, Telegram and more. YouTube is not included in the free plan.</source>
<translation>Бесплатный неограниченный доступ к базовому набору сайтов и приложений, таким как Facebook, Instagram, Twitter (X), Discord, Telegram и другим. YouTube не включён в бесплатный тариф.</translation>
<translation type="vanished">Бесплатный неограниченный доступ к базовому набору сайтов и приложений, таким как Facebook, Instagram, Twitter (X), Discord, Telegram и другим. YouTube не включён в бесплатный тариф.</translation>
</message>
</context>
<context>
<name>ApiConfigsController</name>
<message>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="413"/>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="448"/>
<source>%1 installed successfully.</source>
<translation>%1 успешно установлен.</translation>
</message>
<message>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="472"/>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="513"/>
<source>API config reloaded</source>
<translation>Конфигурация API перезагружена</translation>
</message>
<message>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="476"/>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="517"/>
<source>Successfully changed the country of connection to %1</source>
<translation>Страна подключения изменена на %1</translation>
</message>
@@ -627,27 +624,32 @@ Thank you for staying with us!</source>
<translation>Продолжить</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageHome.qml" line="102"/>
<location filename="../ui/qml/Pages2/PageHome.qml" line="92"/>
<source>Logging enabled</source>
<translation>Логирование включено</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageHome.qml" line="144"/>
<location filename="../ui/qml/Pages2/PageHome.qml" line="120"/>
<source>Dev gateway enabled</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageHome.qml" line="162"/>
<source>Split tunneling enabled</source>
<translation>Раздельное туннелирование включено</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageHome.qml" line="144"/>
<location filename="../ui/qml/Pages2/PageHome.qml" line="162"/>
<source>Split tunneling disabled</source>
<translation>Раздельное туннелирование выключено</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageHome.qml" line="381"/>
<location filename="../ui/qml/Pages2/PageHome.qml" line="409"/>
<source>VPN protocol</source>
<translation>VPN-протокол</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageHome.qml" line="434"/>
<location filename="../ui/qml/Pages2/PageHome.qml" line="462"/>
<source>Servers</source>
<translation>Серверы</translation>
</message>
@@ -1579,32 +1581,37 @@ Thank you for staying with us!</source>
<translation>Настройки</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettings.qml" line="101"/>
<location filename="../ui/qml/Pages2/PageSettings.qml" line="102"/>
<source>Servers</source>
<translation>Серверы</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettings.qml" line="112"/>
<location filename="../ui/qml/Pages2/PageSettings.qml" line="113"/>
<source>Connection</source>
<translation>Соединение</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettings.qml" line="123"/>
<location filename="../ui/qml/Pages2/PageSettings.qml" line="124"/>
<source>Application</source>
<translation>Приложение</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettings.qml" line="134"/>
<location filename="../ui/qml/Pages2/PageSettings.qml" line="135"/>
<source>News &amp; Notifications</source>
<translation>Новости и Уведомления</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettings.qml" line="152"/>
<source>Backup</source>
<translation>Резервное копирование</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettings.qml" line="145"/>
<location filename="../ui/qml/Pages2/PageSettings.qml" line="163"/>
<source>About AmneziaVPN</source>
<translation>Об AmneziaVPN</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettings.qml" line="156"/>
<location filename="../ui/qml/Pages2/PageSettings.qml" line="174"/>
<source>Dev console</source>
<translation>Dev console</translation>
</message>
@@ -2079,52 +2086,40 @@ Thank you for staying with us!</source>
<context>
<name>PageSettingsApiSubscriptionKey</name>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="43"/>
<source>Amnezia Premium
subscription key</source>
<translation>Amnezia Premium
ключ подключения</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="56"/>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="85"/>
<source>Copy key</source>
<translation>Скопировать ключ</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="61"/>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="90"/>
<source>Copied</source>
<translation>Скопировано</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="77"/>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="106"/>
<source>Save key as a file</source>
<translation>Сохранить ключ как файл</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="84"/>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="113"/>
<source>Save AmneziaVPN config</source>
<translation>Сохранить конфигурацию AmneziaVPN</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="85"/>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="114"/>
<source>Config files (*.vpn)</source>
<translation>Файлы конфигов (*.vpn)</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="110"/>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="139"/>
<source>Show key text</source>
<translation>Показать ключ</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="147"/>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="180"/>
<source>To read the QR code in the Amnezia app, tap + in the main menu &apos;QR code&apos;</source>
<translation>Для считывания QR-кода в приложении Amnezia выберите + в главном меню &apos;QR-код&apos;</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="176"/>
<source>Amnezia Premium Subscription key</source>
<translation>Ключ подключения Amnezia Premium</translation>
</message>
</context>
<context>
<name>PageSettingsApiSupport</name>
@@ -2197,32 +2192,37 @@ subscription key</source>
<translation>Режим</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="185"/>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="155"/>
<source>Only &quot;Apps from the list should not have access via VPN&quot; mode is available on Windows</source>
<translation>На Windows доступен только режим &quot;Приложения из списка не должны работать через VPN&quot;</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="199"/>
<source>Remove </source>
<translation>Удалить </translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="186"/>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="200"/>
<source>Continue</source>
<translation>Продолжить</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="187"/>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="201"/>
<source>Cancel</source>
<translation>Отменить</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="228"/>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="242"/>
<source>application name</source>
<translation>название приложения</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="238"/>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="252"/>
<source>Open executable file</source>
<translation>Открыть исполняемый файл</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="239"/>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="253"/>
<source>Executable files (*.*)</source>
<translation>Исполняемые файлы (*.*)</translation>
</message>
@@ -2770,6 +2770,14 @@ subscription key</source>
<translation>Очистить логи</translation>
</message>
</context>
<context>
<name>PageSettingsNewsNotifications</name>
<message>
<location filename="../ui/qml/Pages2/PageSettingsNewsNotifications.qml" line="33"/>
<source>News &amp; Notifications</source>
<translation>Новости и Уведомления</translation>
</message>
</context>
<context>
<name>PageSettingsServerData</name>
<message>
@@ -3013,19 +3021,19 @@ subscription key</source>
<translation>Режим</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="207"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="209"/>
<source>Remove </source>
<translation>Удалить </translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="208"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="356"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="210"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="357"/>
<source>Continue</source>
<translation>Продолжить</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="209"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="357"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="211"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="358"/>
<source>Cancel</source>
<translation>Отменить</translation>
</message>
@@ -3040,70 +3048,70 @@ subscription key</source>
<translation>Невозможно изменить настройки раздельного туннелирования во время активного соединения</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="257"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="259"/>
<source>website or IP</source>
<translation>веб-сайт или IP</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="302"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="304"/>
<source>Additional options</source>
<translation>Дополнительные настройки</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="309"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="311"/>
<source>Import</source>
<translation>Импорт</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="322"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="324"/>
<source>Save site list</source>
<translation>Сохранить список сайтов</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="329"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="331"/>
<source>Save sites</source>
<translation>Сохранить сайты</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="330"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="457"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="470"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="332"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="458"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="471"/>
<source>Sites files (*.json)</source>
<translation>Файлы сайтов (*.json)</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="350"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="352"/>
<source>Clear site list</source>
<translation>Очистить список сайтов</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="354"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="355"/>
<source>Clear site list?</source>
<translation>Очистить список сайтов?</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="355"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="356"/>
<source>All sites will be removed from list.</source>
<translation>Все сайты будут удалены из списка.</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="419"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="420"/>
<source>Import a list of sites</source>
<translation>Импортировать список с сайтами</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="454"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="455"/>
<source>Replace site list</source>
<translation>Заменить список с сайтами</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="456"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="469"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="457"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="470"/>
<source>Open sites file</source>
<translation>Открыть список с сайтами</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="467"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="468"/>
<source>Add imported sites to existing ones</source>
<translation>Добавить импортированные сайты к существующим</translation>
</message>
@@ -3394,38 +3402,38 @@ subscription key</source>
<context>
<name>PageSetupWizardInstalling</name>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="59"/>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="63"/>
<source>The server has already been added to the application</source>
<translation>Сервер уже был добавлен в приложение</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="65"/>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="69"/>
<source>Amnezia has detected that your server is currently </source>
<translation>Amnezia обнаружила, что ваш сервер в настоящее время </translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="66"/>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="70"/>
<source>busy installing other software. Amnezia installation </source>
<translation>занят установкой других протоколов или сервисов. Установка Amnezia </translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="67"/>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="71"/>
<source>will pause until the server finishes installing other software</source>
<translation>будет приостановлена до тех пор, пока сервер не завершит установку другого ПО</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="106"/>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="110"/>
<source>Installing</source>
<translation>Установка</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="151"/>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="155"/>
<source>Cancel installation</source>
<translation>Отменить установку</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="22"/>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="71"/>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="75"/>
<source>Usually it takes no more than 5 minutes</source>
<translation>Обычно это занимает не более 5 минут</translation>
</message>
@@ -3528,32 +3536,32 @@ subscription key</source>
<context>
<name>PageSetupWizardViewConfig</name>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardViewConfig.qml" line="70"/>
<location filename="../ui/qml/Pages2/PageSetupWizardViewConfig.qml" line="72"/>
<source>New connection</source>
<translation>Новое соединение</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardViewConfig.qml" line="110"/>
<location filename="../ui/qml/Pages2/PageSetupWizardViewConfig.qml" line="112"/>
<source>Collapse content</source>
<translation>Свернуть</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardViewConfig.qml" line="110"/>
<location filename="../ui/qml/Pages2/PageSetupWizardViewConfig.qml" line="112"/>
<source>Show content</source>
<translation>Показать</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardViewConfig.qml" line="127"/>
<location filename="../ui/qml/Pages2/PageSetupWizardViewConfig.qml" line="129"/>
<source>Enable WireGuard obfuscation. It may be useful if WireGuard is blocked on your provider.</source>
<translation>Включить обфускацию WireGuard. Это может быть полезно, если WireGuard блокируется вашим провайдером.</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardViewConfig.qml" line="158"/>
<location filename="../ui/qml/Pages2/PageSetupWizardViewConfig.qml" line="160"/>
<source>Use connection codes only from sources you trust. Codes from public sources may have been created to intercept your data.</source>
<translation>Используйте файлы конфигурации только из тех источников, которым вы доверяете. Файлы из общедоступных источников могли быть созданы с целью перехвата ваших личных данных.</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardViewConfig.qml" line="202"/>
<location filename="../ui/qml/Pages2/PageSetupWizardViewConfig.qml" line="204"/>
<source>Connect</source>
<translation>Подключиться</translation>
</message>
@@ -3561,217 +3569,217 @@ subscription key</source>
<context>
<name>PageShare</name>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="130"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="125"/>
<source>OpenVPN native format</source>
<translation>Оригинальный формат OpenVPN</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="135"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="130"/>
<source>WireGuard native format</source>
<translation>Оригинальный формат WireGuard</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="260"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="255"/>
<source>Connection</source>
<translation>Соединение</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="328"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="329"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="323"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="324"/>
<source>Server</source>
<translation>Сервер</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="39"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="37"/>
<source>Config revoked</source>
<translation>Конфигурация отозвана</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="55"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="50"/>
<source>Save AmneziaVPN config</source>
<translation>Сохранить конфигурацию AmneziaVPN</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="62"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="57"/>
<source>Save OpenVPN config</source>
<translation>Сохранить конфигурацию OpenVPN</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="69"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="64"/>
<source>Save WireGuard config</source>
<translation>Сохранить конфигурацию WireGuard</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="76"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="71"/>
<source>Save AmneziaWG config</source>
<translation>Сохранить конфигурацию AmneziaWG</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="83"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="78"/>
<source>Save Shadowsocks config</source>
<translation>Сохранить конфигурацию Shadowsocks</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="90"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="85"/>
<source>Save Cloak config</source>
<translation>Сохранить конфигурацию Cloak</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="97"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="92"/>
<source>Save XRay config</source>
<translation>Сохранить конфигурацию XRay</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="106"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="101"/>
<source>Connection to </source>
<translation>Подключение к </translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="107"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="102"/>
<source>File with connection settings to </source>
<translation>Файл с настройками подключения к </translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="125"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="120"/>
<source>For the AmneziaVPN app</source>
<translation>Для приложения AmneziaVPN</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="140"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="135"/>
<source>AmneziaWG native format</source>
<translation>Оригинальный формат AmneziaWG</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="145"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="140"/>
<source>Shadowsocks native format</source>
<translation>Оригинальный формат Shadowsocks</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="150"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="145"/>
<source>Cloak native format</source>
<translation>Оригинальный формат Cloak</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="155"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="150"/>
<source>XRay native format</source>
<translation>Оригинальный формат XRay</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="183"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="178"/>
<source>Share VPN Access</source>
<translation>Поделиться VPN</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="217"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="212"/>
<source>Share full access to the server and VPN</source>
<translation>Поделиться полным доступом к серверу и VPN</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="218"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="213"/>
<source>Use for your own devices, or share with those you trust to manage the server.</source>
<translation>Используйте для собственных устройств или передайте управление сервером тем, кому вы доверяете.</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="275"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="555"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="270"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="550"/>
<source>Users</source>
<translation>Пользователи</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="309"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="304"/>
<source>User name</source>
<translation>Имя пользователя</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="571"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="566"/>
<source>Search</source>
<translation>Поиск</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="694"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="691"/>
<source>Creation date: %1</source>
<translation>Дата создания: %1</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="706"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="703"/>
<source>Latest handshake: %1</source>
<translation>Последнее рукопожатие: %1</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="718"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="715"/>
<source>Data received: %1</source>
<translation>Получено данных: %1</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="730"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="727"/>
<source>Data sent: %1</source>
<translation>Отправлено данных: %1</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="740"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="737"/>
<source>Allowed IPs: %1</source>
<translation>Разрешенные подсети: %1</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="755"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="752"/>
<source>Rename</source>
<translation>Переименовать</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="780"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="777"/>
<source>Client name</source>
<translation>Имя клиента</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="791"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="788"/>
<source>Save</source>
<translation>Сохранить</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="825"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="824"/>
<source>Revoke</source>
<translation>Отозвать</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="828"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="827"/>
<source>Revoke the config for a user - %1?</source>
<translation>Отозвать конфигурацию для пользователя - %1?</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="829"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="828"/>
<source>The user will no longer be able to connect to your server.</source>
<translation>Пользователь больше не сможет подключаться к вашему серверу.</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="830"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="829"/>
<source>Continue</source>
<translation>Продолжить</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="831"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="830"/>
<source>Cancel</source>
<translation>Отменить</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="298"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="293"/>
<source>Share VPN access without the ability to manage the server</source>
<translation>Поделиться доступом к VPN без возможности управления сервером</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="389"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="390"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="384"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="385"/>
<source>Protocol</source>
<translation>Протокол</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="496"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="497"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="491"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="492"/>
<source>Connection format</source>
<translation>Формат подключения</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="225"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="537"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="220"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="532"/>
<source>Share</source>
<translation>Поделиться</translation>
</message>
@@ -3810,7 +3818,7 @@ subscription key</source>
<translation>Скопировано</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShareConnection.qml" line="319"/>
<location filename="../ui/qml/Pages2/PageShareConnection.qml" line="323"/>
<source>To read the QR code in the Amnezia app, select &quot;Add server&quot; &quot;I have data to connect&quot; &quot;QR code, key or settings file&quot;</source>
<translation>Для считывания QR-кода в приложении Amnezia выберите &quot;Добавить сервер&quot; &quot;У меня есть данные для подключения&quot; &quot;Открыть файл конфигурации, ключ или QR-код&quot;</translation>
</message>
@@ -3841,22 +3849,22 @@ subscription key</source>
<translation>Сервер</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="114"/>
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="115"/>
<source>Accessing </source>
<translation>Доступ </translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="115"/>
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="116"/>
<source>File with accessing settings to </source>
<translation>Файл с настройками доступа к </translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="146"/>
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="147"/>
<source>Share</source>
<translation>Поделиться</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="154"/>
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="155"/>
<source>Access error!</source>
<translation>Ошибка доступа!</translation>
</message>
@@ -4957,12 +4965,12 @@ FileZilla или другие SFTP-клиенты, а также смонтир
<context>
<name>SettingsController</name>
<message>
<location filename="../ui/controllers/settingsController.cpp" line="242"/>
<location filename="../ui/controllers/settingsController.cpp" line="258"/>
<source>All settings have been reset to default values</source>
<translation>Все настройки сброшены до значений по умолчанию</translation>
</message>
<message>
<location filename="../ui/controllers/settingsController.cpp" line="219"/>
<location filename="../ui/controllers/settingsController.cpp" line="235"/>
<source>Backup file is corrupted</source>
<translation>Файл резервной копии поврежден</translation>
</message>
@@ -5051,7 +5059,7 @@ FileZilla или другие SFTP-клиенты, а также смонтир
<context>
<name>TextFieldWithHeaderType</name>
<message>
<location filename="../ui/qml/Controls2/TextFieldWithHeaderType.qml" line="117"/>
<location filename="../ui/qml/Controls2/TextFieldWithHeaderType.qml" line="133"/>
<source>The field can&apos;t be empty</source>
<translation>Поле не может быть пустым</translation>
</message>
@@ -29,7 +29,6 @@ namespace
constexpr char uuid[] = "installation_uuid";
constexpr char osVersion[] = "os_version";
constexpr char appVersion[] = "app_version";
constexpr char appLanguage[] = "app_language";
constexpr char userCountryCode[] = "user_country_code";
constexpr char serverCountryCode[] = "server_country_code";
@@ -47,6 +46,8 @@ namespace
constexpr char subscription[] = "subscription";
constexpr char endDate[] = "end_date";
constexpr char isConnectEvent[] = "is_connect_event";
}
struct ProtocolData
@@ -63,6 +64,7 @@ namespace
{
QString osVersion;
QString appVersion;
QString appLanguage;
QString installationUuid;
@@ -82,6 +84,9 @@ namespace
if (!appVersion.isEmpty()) {
obj[configKey::appVersion] = appVersion;
}
if (!appLanguage.isEmpty()) {
obj[apiDefs::key::appLanguage] = appLanguage;
}
if (!installationUuid.isEmpty()) {
obj[configKey::uuid] = installationUuid;
}
@@ -221,6 +226,9 @@ namespace
if (newServerConfig.value(config_key::configVersion).toInt() == apiDefs::ConfigSource::AmneziaGateway) {
apiConfig.insert(apiDefs::key::supportedProtocols,
QJsonDocument::fromJson(apiResponseBody).object().value(apiDefs::key::supportedProtocols).toArray());
apiConfig.insert(apiDefs::key::serviceInfo,
QJsonDocument::fromJson(apiResponseBody).object().value(apiDefs::key::serviceInfo).toObject());
}
serverConfig[configKey::apiConfig] = apiConfig;
@@ -249,6 +257,23 @@ ApiConfigsController::ApiConfigsController(const QSharedPointer<ServersModel> &s
{
}
bool ApiConfigsController::exportVpnKey(const QString &fileName)
{
if (fileName.isEmpty()) {
emit errorOccurred(ErrorCode::PermissionsError);
return false;
}
prepareVpnKeyExport();
if (m_vpnKey.isEmpty()) {
emit errorOccurred(ErrorCode::ApiConfigEmptyError);
return false;
}
SystemController::saveFile(fileName, m_vpnKey);
return true;
}
bool ApiConfigsController::exportNativeConfig(const QString &serverCountryCode, const QString &fileName)
{
if (fileName.isEmpty()) {
@@ -266,6 +291,7 @@ bool ApiConfigsController::exportNativeConfig(const QString &serverCountryCode,
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
m_settings->getInstallationUuid(true),
apiConfigObject.value(configKey::userCountryCode).toString(),
serverCountryCode,
@@ -306,6 +332,7 @@ bool ApiConfigsController::revokeNativeConfig(const QString &serverCountryCode)
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
m_settings->getInstallationUuid(true),
apiConfigObject.value(configKey::userCountryCode).toString(),
serverCountryCode,
@@ -330,6 +357,13 @@ void ApiConfigsController::prepareVpnKeyExport()
auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject();
auto vpnKey = apiConfigObject.value(apiDefs::key::vpnKey).toString();
if (vpnKey.isEmpty()) {
vpnKey = apiUtils::getPremiumV2VpnKey(serverConfigObject);
apiConfigObject.insert(apiDefs::key::vpnKey, vpnKey);
serverConfigObject.insert(configKey::apiConfig, apiConfigObject);
m_serversModel->editServer(serverConfigObject, m_serversModel->getProcessedServerIndex());
}
m_vpnKey = vpnKey;
vpnKey.replace("vpn://", "");
@@ -349,7 +383,7 @@ bool ApiConfigsController::fillAvailableServices()
{
QJsonObject apiPayload;
apiPayload[configKey::osVersion] = QSysInfo::productType();
apiPayload[configKey::appLanguage] = m_settings->getAppLanguage().name().split("_").first();
apiPayload[apiDefs::key::appLanguage] = m_settings->getAppLanguage().name().split("_").first();
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/services"), apiPayload, responseBody);
@@ -373,6 +407,7 @@ bool ApiConfigsController::importServiceFromGateway()
{
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
m_settings->getInstallationUuid(true),
m_apiServicesModel->getCountryCode(),
"",
@@ -431,6 +466,7 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
m_settings->getInstallationUuid(true),
apiConfig.value(configKey::userCountryCode).toString(),
newCountryCode,
@@ -443,6 +479,10 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
appendProtocolDataToApiPayload(gatewayRequestData.serviceProtocol, protocolData, apiPayload);
if (newCountryCode.isEmpty() && newCountryName.isEmpty() && !reloadServiceConfig) {
apiPayload.insert(configKey::isConnectEvent, true);
}
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody);
@@ -526,7 +566,7 @@ bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex)
}
}
bool ApiConfigsController::deactivateDevice()
bool ApiConfigsController::deactivateDevice(const bool isRemoveEvent)
{
auto serverIndex = m_serversModel->getProcessedServerIndex();
auto serverConfigObject = m_serversModel->getServerConfig(serverIndex);
@@ -537,12 +577,17 @@ bool ApiConfigsController::deactivateDevice()
}
if (isSubscriptionExpired(apiConfigObject)) {
emit errorOccurred(ErrorCode::ApiSubscriptionExpiredError);
return false;
if (isRemoveEvent) {
return true;
} else {
emit errorOccurred(ErrorCode::ApiSubscriptionExpiredError);
return false;
}
}
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
m_settings->getInstallationUuid(true),
apiConfigObject.value(configKey::userCountryCode).toString(),
apiConfigObject.value(configKey::serverCountryCode).toString(),
@@ -582,6 +627,7 @@ bool ApiConfigsController::deactivateExternalDevice(const QString &uuid, const Q
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
uuid,
apiConfigObject.value(configKey::userCountryCode).toString(),
serverCountryCode,
@@ -21,7 +21,7 @@ public:
public slots:
bool exportNativeConfig(const QString &serverCountryCode, const QString &fileName);
bool revokeNativeConfig(const QString &serverCountryCode);
// bool exportVpnKey(const QString &fileName);
bool exportVpnKey(const QString &fileName);
void prepareVpnKeyExport();
void copyVpnKeyToClipboard();
@@ -30,7 +30,7 @@ public slots:
bool updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName,
bool reloadServiceConfig = false);
bool updateServiceFromTelegram(const int serverIndex);
bool deactivateDevice();
bool deactivateDevice(const bool isRemoveEvent);
bool deactivateExternalDevice(const QString &uuid, const QString &serverCountryCode);
bool isConfigValid();
@@ -0,0 +1,69 @@
#include "apiNewsController.h"
#include "core/api/apiUtils.h"
#include <QJsonDocument>
#include <QJsonObject>
namespace
{
namespace configKey
{
constexpr char userCountryCode[] = "user_country_code";
constexpr char serviceType[] = "service_type";
}
}
ApiNewsController::ApiNewsController(const QSharedPointer<NewsModel> &newsModel, const std::shared_ptr<Settings> &settings,
const QSharedPointer<ServersModel> &serversModel, QObject *parent)
: QObject(parent), m_newsModel(newsModel), m_settings(settings), m_serversModel(serversModel)
{
}
void ApiNewsController::fetchNews(bool showError)
{
if (m_serversModel.isNull()) {
qWarning() << "ServersModel is null, skip fetchNews";
return;
}
const auto stacks = m_serversModel->gatewayStacks();
if (stacks.isEmpty()) {
qDebug() << "No Gateway stacks, skip fetchNews";
return;
}
auto gatewayController = QSharedPointer<GatewayController>::create(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(),
apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
QJsonObject payload;
payload.insert("locale", m_settings->getAppLanguage().name().split("_").first());
const QJsonObject stacksJson = stacks.toJson();
if (stacksJson.contains(configKey::userCountryCode)) {
payload.insert(configKey::userCountryCode, stacksJson.value(configKey::userCountryCode));
}
if (stacksJson.contains(configKey::serviceType)) {
payload.insert(configKey::serviceType, stacksJson.value(configKey::serviceType));
}
auto future = gatewayController->postAsync(QString("%1v1/news"), payload);
future.then(this, [this, showError, gatewayController](QPair<ErrorCode, QByteArray> result) {
auto [errorCode, responseBody] = result;
if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode, showError);
return;
}
QJsonDocument doc = QJsonDocument::fromJson(responseBody);
QJsonArray newsArray;
if (doc.isArray()) {
newsArray = doc.array();
} else if (doc.isObject()) {
QJsonObject obj = doc.object();
if (obj.value("news").isArray()) {
newsArray = obj.value("news").toArray();
}
}
m_newsModel->updateModel(newsArray);
emit fetchNewsFinished();
});
}
@@ -0,0 +1,34 @@
#ifndef APINEWSCONTROLLER_H
#define APINEWSCONTROLLER_H
#include <QJsonArray>
#include <QObject>
#include <QSharedPointer>
#include <memory>
#include "core/api/apiDefs.h"
#include "core/controllers/gatewayController.h"
#include "settings.h"
#include "ui/models/newsModel.h"
#include "ui/models/servers_model.h"
class ApiNewsController : public QObject
{
Q_OBJECT
public:
explicit ApiNewsController(const QSharedPointer<NewsModel> &newsModel, const std::shared_ptr<Settings> &settings,
const QSharedPointer<ServersModel> &serversModel, QObject *parent = nullptr);
Q_INVOKABLE void fetchNews(bool showError);
signals:
void errorOccurred(ErrorCode errorCode, bool showError);
void fetchNewsFinished();
private:
QSharedPointer<NewsModel> m_newsModel;
std::shared_ptr<Settings> m_settings;
QSharedPointer<ServersModel> m_serversModel;
};
#endif // APINEWSCONTROLLER_H
@@ -82,7 +82,7 @@ void ApiPremV1MigrationController::sendMigrationCode(const int subscriptionIndex
{
QEventLoop wait;
QTimer::singleShot(1000, &wait, &QEventLoop::quit);
wait.exec();
wait.exec(QEventLoop::ExcludeUserInputEvents);
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
@@ -46,7 +46,7 @@ bool ApiSettingsController::getAccountInfo(bool reload)
if (reload) {
QEventLoop wait;
QTimer::singleShot(1000, &wait, &QEventLoop::quit);
wait.exec();
wait.exec(QEventLoop::ExcludeUserInputEvents);
}
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), requestTimeoutMsecs,
@@ -62,6 +62,7 @@ bool ApiSettingsController::getAccountInfo(bool reload)
apiPayload[configKey::serviceType] = apiConfig.value(configKey::serviceType).toString();
apiPayload[configKey::authData] = authData;
apiPayload[apiDefs::key::cliVersion] = QString(APP_VERSION);
apiPayload[apiDefs::key::appLanguage] = m_settings->getAppLanguage().name().split("_").first();
QByteArray responseBody;
+69 -1
View File
@@ -10,7 +10,10 @@
#include "core/controllers/vpnConfigurationController.h"
#include "core/qrCodeUtils.h"
#include "core/serialization/serialization.h"
#include "core/serialization/transfer.h"
#include "systemController.h"
#include <QDebug>
ExportController::ExportController(const QSharedPointer<ServersModel> &serversModel, const QSharedPointer<ContainersModel> &containersModel,
const QSharedPointer<ClientManagementModel> &clientManagementModel,
@@ -249,6 +252,7 @@ void ExportController::generateCloakConfig()
void ExportController::generateXrayConfig(const QString &clientName)
{
//Xray data
QJsonObject nativeConfig;
ErrorCode errorCode = generateNativeConfig(DockerContainer::Xray, clientName, Proto::Xray, nativeConfig);
if (errorCode) {
@@ -258,8 +262,72 @@ void ExportController::generateXrayConfig(const QString &clientName)
QStringList lines = QString(QJsonDocument(nativeConfig).toJson()).replace("\r", "").split("\n");
for (const QString &line : std::as_const(lines)) {
m_config.append(line + "\n");
m_config.append(line+ "\n");
}
//Xray data
// Parse the Xray data to extract VLESS parameters and generate string
QString configString = QString(QJsonDocument(nativeConfig).toJson(QJsonDocument::Compact));
QJsonDocument doc = QJsonDocument::fromJson(configString.toUtf8());
if (doc.isNull() || !doc.isObject()) {
qDebug() << "ERROR: Failed to parse config JSON";
emit exportErrorOccurred(ErrorCode::InternalError);
return;
}
QJsonObject xrayConfig = doc.object();
QJsonArray outbounds = xrayConfig.value("outbounds").toArray();
if (outbounds.isEmpty()) {
qDebug() << "ERROR: Outbounds array is empty";
emit exportErrorOccurred(ErrorCode::InternalError);
return;
}
QJsonObject outbound = outbounds[0].toObject();
QJsonObject settings = outbound.value("settings").toObject();
QJsonObject streamSettings = outbound.value("streamSettings").toObject();
QJsonArray vnext = settings.value("vnext").toArray();
if (vnext.isEmpty()) {
qDebug() << "ERROR: vnext array is empty";
emit exportErrorOccurred(ErrorCode::InternalError);
return;
}
QJsonObject server = vnext[0].toObject();
QJsonArray users = server.value("users").toArray();
if (users.isEmpty()) {
qDebug() << "ERROR: users array is empty";
emit exportErrorOccurred(ErrorCode::InternalError);
return;
}
QJsonObject user = users[0].toObject();
amnezia::serialization::VlessServerObject vlessServer;
vlessServer.address = server.value("address").toString();
vlessServer.port = server.value("port").toInt();
vlessServer.id = user.value("id").toString();
vlessServer.flow = user.value("flow").toString("xtls-rprx-vision");
vlessServer.encryption = user.value("encryption").toString("none");
vlessServer.network = streamSettings.value("network").toString("tcp");
vlessServer.security = streamSettings.value("security").toString("reality");
if (vlessServer.security == "reality") {
QJsonObject realitySettings = streamSettings.value("realitySettings").toObject();
vlessServer.serverName = realitySettings.value("serverName").toString();
vlessServer.publicKey = realitySettings.value("publicKey").toString();
vlessServer.shortId = realitySettings.value("shortId").toString();
vlessServer.fingerprint = realitySettings.value("fingerprint").toString("chrome");
vlessServer.spiderX = realitySettings.value("spiderX").toString("");
}
m_nativeConfigString = amnezia::serialization::vless::Serialize(vlessServer, "AmneziaVPN");
emit exportConfigChanged();
}
+7 -1
View File
@@ -208,6 +208,7 @@ bool ImportController::extractConfigFromData(QString data)
}
case ConfigTypes::Invalid: {
emit importErrorOccurred(ErrorCode::ImportInvalidConfigError, false);
m_configFileName.clear();
break;
}
}
@@ -274,7 +275,7 @@ void ImportController::processNativeWireGuardConfig()
auto serverProtocolConfig = container.value(ContainerProps::containerTypeToString(DockerContainer::WireGuard)).toObject();
auto clientProtocolConfig = QJsonDocument::fromJson(serverProtocolConfig.value(config_key::last_config).toString().toUtf8()).object();
QString junkPacketCount = QString::number(QRandomGenerator::global()->bounded(2, 5));
QString junkPacketCount = QString::number(QRandomGenerator::global()->bounded(4, 7));
QString junkPacketMinSize = QString::number(10);
QString junkPacketMaxSize = QString::number(50);
clientProtocolConfig[config_key::junkPacketCount] = junkPacketCount;
@@ -341,6 +342,11 @@ void ImportController::importConfig()
m_maliciousWarningText.clear();
}
void ImportController::clearConfigFileName()
{
m_configFileName.clear();
}
QJsonObject ImportController::extractOpenVpnConfig(const QString &data)
{
QJsonObject openVpnConfig;
+1
View File
@@ -30,6 +30,7 @@ public:
public slots:
void importConfig();
void clearConfigFileName();
bool extractConfigFromFile(const QString &fileName);
bool extractConfigFromData(QString data);
bool extractConfigFromQr(const QByteArray &data);
+1 -1
View File
@@ -73,7 +73,7 @@ void InstallController::install(DockerContainer container, int port, TransportPr
containerConfig.insert(config_key::transport_proto, ProtocolProps::transportProtoToString(transportProto, protocol));
if (container == DockerContainer::Awg) {
QString junkPacketCount = QString::number(QRandomGenerator::global()->bounded(2, 5));
QString junkPacketCount = QString::number(QRandomGenerator::global()->bounded(4, 7));
QString junkPacketMinSize = QString::number(10);
QString junkPacketMaxSize = QString::number(50);
+1 -1
View File
@@ -169,7 +169,7 @@ void PageController::onShowErrorMessage(ErrorCode errorCode)
{
const auto fullErrorMessage = errorString(errorCode);
const auto errorMessage = fullErrorMessage.mid(fullErrorMessage.indexOf(". ") + 1); // remove ErrorCode %1.
const auto errorUrl = QStringLiteral("https://docs.amnezia.org/troubleshooting/error-codes/#error-%1-%2").arg(static_cast<int>(errorCode)).arg(utils::enumToString(errorCode).toLower());
const auto errorUrl = QStringLiteral("troubleshooting/error-codes/#error-%1-%2").arg(static_cast<int>(errorCode)).arg(utils::enumToString(errorCode).toLower());
const auto fullMessage = QStringLiteral("<a href=\"%1\" style=\"color: #FBB26A;\">ErrorCode: %2</a>. %3").arg(errorUrl).arg(static_cast<int>(errorCode)).arg(errorMessage);
emit showErrorMessage(fullMessage);
+2
View File
@@ -26,6 +26,8 @@ namespace PageLoader
PageSettingsConnection,
PageSettingsDns,
PageSettingsApplication,
PageSettingsNewsNotifications,
PageSettingsNewsDetail,
PageSettingsBackup,
PageSettingsAbout,
PageSettingsLogging,
+101 -3
View File
@@ -1,10 +1,12 @@
#include "settingsController.h"
#include <QStandardPaths>
#include <QOperatingSystemVersion>
#include "logger.h"
#include "systemController.h"
#include "ui/qautostart.h"
#include "amnezia_application.h"
#include "version.h"
#ifdef Q_OS_ANDROID
#include "platforms/android/android_controller.h"
@@ -32,7 +34,21 @@ SettingsController::SettingsController(const QSharedPointer<ServersModel> &serve
checkIfNeedDisableLogs();
#ifdef Q_OS_ANDROID
connect(AndroidController::instance(), &AndroidController::notificationStateChanged, this, &SettingsController::onNotificationStateChanged);
connect(AndroidController::instance(), &AndroidController::imeInsetsChanged, this, [this](int heightDp) {
m_imeHeight = heightDp;
emit imeHeightChanged(heightDp);
emit safeAreaBottomMarginChanged();
});
connect(AndroidController::instance(), &AndroidController::systemBarsInsetsChanged, this, [this](int navBarHeightDp, int statusBarHeightDp) {
m_cachedNavigationBarHeight = navBarHeightDp;
m_cachedStatusBarHeight = statusBarHeightDp;
emit safeAreaBottomMarginChanged();
emit safeAreaTopMarginChanged();
});
#endif
m_isDevModeEnabled = m_settings->isDevGatewayEnv();
toggleDevGatewayEnv(m_isDevModeEnabled);
}
QString getPlatformName()
@@ -139,6 +155,10 @@ void SettingsController::clearLogs()
Logger::clearLogs(false);
Logger::clearServiceLogs();
#endif
qInfo().noquote() << QString("Started %1 version %2 %3").arg(APPLICATION_NAME, APP_VERSION, GIT_COMMIT_HASH);
qInfo().noquote() << QString("%1 (%2)").arg(QSysInfo::prettyProductName(), QSysInfo::currentCpuArchitecture());
qInfo().noquote() << QString("SSL backend: %1").arg(QSslSocket::sslLibraryVersionString());
}
void SettingsController::backupAppConfig(const QString &fileName)
@@ -151,6 +171,7 @@ void SettingsController::backupAppConfig(const QString &fileName)
config["Conf/autoStart"] = Autostart::isAutostart();
config["Conf/killSwitchEnabled"] = isKillSwitchEnabled();
config["Conf/strictKillSwitchEnabled"] = isStrictKillSwitchEnabled();
config["Conf/useAmneziaDns"] = isAmneziaDnsEnabled();
SystemController::saveFile(fileName, QJsonDocument(config).toJson());
}
@@ -186,7 +207,8 @@ void SettingsController::restoreAppConfigFromData(const QByteArray &data)
#if defined(Q_OS_WINDOWS) || defined(Q_OS_ANDROID)
int appSplitTunnelingRouteMode = newConfigData.value("Conf/appsRouteMode").toInt();
bool appSplittunnelingEnabled = newConfigData.value("Conf/appsSplitTunnelingEnabled").toString().toLower() == "true";
bool appSplittunnelingEnabled =
newConfigData.value("Conf/appsSplitTunnelingEnabled").toVariant().toString().toLower() == "true";
m_appSplitTunnelingModel->setRouteMode(appSplitTunnelingRouteMode);
#if defined(Q_OS_WINDOWS)
@@ -198,12 +220,13 @@ void SettingsController::restoreAppConfigFromData(const QByteArray &data)
m_appSplitTunnelingModel->clearAppsList();
}
}
m_appSplitTunnelingModel->toggleSplitTunneling(appSplittunnelingEnabled);
#endif
int siteSplitTunnelingRouteMode = newConfigData.value("Conf/routeMode").toInt();
bool siteSplittunnelingEnabled = newConfigData.value("Conf/sitesSplitTunnelingEnabled").toString().toLower() == "true";
bool siteSplittunnelingEnabled =
newConfigData.value("Conf/sitesSplitTunnelingEnabled").toVariant().toString().toLower() == "true";
m_sitesModel->setRouteMode(siteSplitTunnelingRouteMode);
m_sitesModel->toggleSplitTunneling(siteSplittunnelingEnabled);
@@ -214,6 +237,11 @@ void SettingsController::restoreAppConfigFromData(const QByteArray &data)
m_settings->setStrictKillSwitchEnabled(false);
#endif
bool amneziaDnsEnabled = newConfigData.contains("Conf/useAmneziaDns")
? newConfigData.value("Conf/useAmneziaDns").toBool()
: m_settings->useAmneziaDns();
emit amneziaDnsToggled(amneziaDnsEnabled);
emit restoreBackupFinished();
} else {
emit changeSettingsErrorOccurred(tr("Backup file is corrupted"));
@@ -415,6 +443,76 @@ bool SettingsController::isOnTv()
#endif
}
bool SettingsController::isEdgeToEdgeEnabled()
{
#ifdef Q_OS_ANDROID
if (!m_edgeToEdgeCached) {
m_cachedEdgeToEdgeEnabled = AndroidController::instance()->isEdgeToEdgeEnabled();
m_edgeToEdgeCached = true;
}
return m_cachedEdgeToEdgeEnabled;
#else
return false;
#endif
}
int SettingsController::getStatusBarHeight()
{
#ifdef Q_OS_ANDROID
if (m_cachedStatusBarHeight < 0) {
m_cachedStatusBarHeight = AndroidController::instance()->getStatusBarHeight();
}
return m_cachedStatusBarHeight;
#else
return 0;
#endif
}
int SettingsController::getNavigationBarHeight()
{
#ifdef Q_OS_ANDROID
if (m_cachedNavigationBarHeight < 0) {
m_cachedNavigationBarHeight = AndroidController::instance()->getNavigationBarHeight();
}
return m_cachedNavigationBarHeight;
#else
return 0;
#endif
}
int SettingsController::getSafeAreaTopMargin()
{
#ifdef Q_OS_ANDROID
if (isEdgeToEdgeEnabled()) {
int height = getStatusBarHeight();
int result = height > 0 ? height : 40; // fallback to 40 if system returns 0
return result;
}
#endif
return 0;
}
int SettingsController::getSafeAreaBottomMargin()
{
#ifdef Q_OS_ANDROID
if (isEdgeToEdgeEnabled()) {
if (m_imeHeight > 0) {
return 0;
}
int height = getNavigationBarHeight();
int result = height > 0 ? height : 56; // fallback to 56 if system returns 0
return result;
}
#endif
return 0;
}
int SettingsController::getImeHeight()
{
return m_imeHeight;
}
bool SettingsController::isHomeAdLabelVisible()
{
return m_settings->isHomeAdLabelVisible();
@@ -33,6 +33,9 @@ public:
Q_PROPERTY(bool isHomeAdLabelVisible READ isHomeAdLabelVisible NOTIFY isHomeAdLabelVisibleChanged)
Q_PROPERTY(bool startMinimized READ isStartMinimizedEnabled NOTIFY startMinimizedChanged)
Q_PROPERTY(int safeAreaTopMargin READ getSafeAreaTopMargin NOTIFY safeAreaTopMarginChanged)
Q_PROPERTY(int safeAreaBottomMargin READ getSafeAreaBottomMargin NOTIFY safeAreaBottomMarginChanged)
Q_PROPERTY(int imeHeight READ getImeHeight NOTIFY imeHeightChanged)
public slots:
void toggleAmneziaDns(bool enable);
@@ -96,6 +99,12 @@ public slots:
void toggleDevGatewayEnv(bool enabled);
bool isOnTv();
bool isEdgeToEdgeEnabled();
int getStatusBarHeight();
int getNavigationBarHeight();
int getSafeAreaTopMargin();
int getSafeAreaBottomMargin();
int getImeHeight();
bool isHomeAdLabelVisible();
void disableHomeAdLabel();
@@ -124,6 +133,10 @@ signals:
void devModeEnabled();
void gatewayEndpointChanged(const QString &endpoint);
void devGatewayEnvChanged(bool enabled);
void imeHeightChanged(int height);
void safeAreaTopMarginChanged();
void safeAreaBottomMarginChanged();
void isHomeAdLabelVisibleChanged(bool visible);
void startMinimizedChanged();
@@ -134,6 +147,12 @@ private:
QSharedPointer<LanguageModel> m_languageModel;
QSharedPointer<SitesModel> m_sitesModel;
QSharedPointer<AppSplitTunnelingModel> m_appSplitTunnelingModel;
mutable int m_cachedStatusBarHeight = -1;
mutable int m_cachedNavigationBarHeight = -1;
mutable bool m_cachedEdgeToEdgeEnabled = false;
mutable bool m_edgeToEdgeCached = false;
int m_imeHeight = 0;
std::shared_ptr<Settings> m_settings;
QString m_appVersion;
+5 -11
View File
@@ -31,7 +31,8 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
return tr("Active");
}
return apiUtils::isSubscriptionExpired(m_accountInfoData.subscriptionEndDate) ? tr("<p><a style=\"color: #EB5757;\">Inactive</a>") : tr("Active");
return apiUtils::isSubscriptionExpired(m_accountInfoData.subscriptionEndDate) ? tr("<p><a style=\"color: #EB5757;\">Inactive</a>")
: tr("Active");
}
case EndDateRole: {
if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) {
@@ -47,16 +48,7 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
return tr("%1 out of %2").arg(m_accountInfoData.activeDeviceCount).arg(m_accountInfoData.maxDeviceCount);
}
case ServiceDescriptionRole: {
if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2) {
return tr("Classic VPN for seamless work, downloading large files, and watching videos. Access all websites and online "
"resources. "
"Speeds up to 200 Mbps");
} else if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) {
return tr("Free unlimited access to a basic set of websites such as Facebook, Instagram, Twitter (X), Discord, Telegram and "
"more. YouTube is not included in the free plan.");
} else {
return "";
}
return m_accountInfoData.subscriptionDescription;
}
case IsComponentVisibleRole: {
return m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2
@@ -101,6 +93,8 @@ void ApiAccountInfoModel::updateModel(const QJsonObject &accountInfoObject, cons
accountInfoData.configType = apiUtils::getConfigType(serverConfig);
accountInfoData.subscriptionDescription = accountInfoObject.value(apiDefs::key::subscriptionDescription).toString();
for (const auto &protocol : accountInfoObject.value(apiDefs::key::supportedProtocols).toArray()) {
accountInfoData.supportedProtocols.push_back(protocol.toString());
}
@@ -54,6 +54,8 @@ private:
apiDefs::ConfigType configType;
QStringList supportedProtocols;
QString subscriptionDescription;
};
AccountInfoData m_accountInfoData;
+4
View File
@@ -48,6 +48,7 @@ QString LanguageModel::getLocalLanguageName(const LanguageSettings::AvailableLan
case LanguageSettings::AvailableLanguageEnum::Burmese: strLanguage = "မြန်မာဘာသာ"; break;
case LanguageSettings::AvailableLanguageEnum::Urdu: strLanguage = "اُرْدُوْ"; break;
case LanguageSettings::AvailableLanguageEnum::Hindi: strLanguage = "हिन्दी"; break;
case LanguageSettings::AvailableLanguageEnum::Spanish: strLanguage = "Español"; break;
default: break;
}
@@ -66,6 +67,7 @@ void LanguageModel::changeLanguage(const LanguageSettings::AvailableLanguageEnum
case LanguageSettings::AvailableLanguageEnum::Burmese: emit updateTranslations(QLocale::Burmese); break;
case LanguageSettings::AvailableLanguageEnum::Urdu: emit updateTranslations(QLocale::Urdu); break;
case LanguageSettings::AvailableLanguageEnum::Hindi: emit updateTranslations(QLocale::Hindi); break;
case LanguageSettings::AvailableLanguageEnum::Spanish: emit updateTranslations(QLocale::Spanish); break;
default: emit updateTranslations(QLocale::English); break;
}
}
@@ -83,6 +85,7 @@ int LanguageModel::getCurrentLanguageIndex()
case QLocale::Burmese: return static_cast<int>(LanguageSettings::AvailableLanguageEnum::Burmese); break;
case QLocale::Urdu: return static_cast<int>(LanguageSettings::AvailableLanguageEnum::Urdu); break;
case QLocale::Hindi: return static_cast<int>(LanguageSettings::AvailableLanguageEnum::Hindi); break;
case QLocale::Spanish: return static_cast<int>(LanguageSettings::AvailableLanguageEnum::Spanish); break;
default: return static_cast<int>(LanguageSettings::AvailableLanguageEnum::English); break;
}
}
@@ -113,6 +116,7 @@ LanguageSettings::AvailableLanguageEnum LanguageModel::getSystemLanguageEnum()
case QLocale::Burmese: return LanguageSettings::AvailableLanguageEnum::Burmese;
case QLocale::Urdu: return LanguageSettings::AvailableLanguageEnum::Urdu;
case QLocale::Hindi: return LanguageSettings::AvailableLanguageEnum::Hindi;
case QLocale::Spanish: return LanguageSettings::AvailableLanguageEnum::Spanish;
case QLocale::English: return LanguageSettings::AvailableLanguageEnum::English;
default: return LanguageSettings::AvailableLanguageEnum::English;
}
+2 -1
View File
@@ -18,7 +18,8 @@ namespace LanguageSettings
Arabic,
Burmese,
Urdu,
Hindi
Hindi,
Spanish
};
Q_ENUM_NS(AvailableLanguageEnum)
+124
View File
@@ -0,0 +1,124 @@
#include "ui/models/newsModel.h"
#include <QDir>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QQmlEngine>
#include <QStandardPaths>
#include <algorithm>
NewsModel::NewsModel(const std::shared_ptr<Settings> &settings, QObject *parent) : QAbstractListModel(parent), m_settings(settings)
{
loadReadIds();
}
int NewsModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return m_items.size();
}
QVariant NewsModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= m_items.size())
return QVariant();
const NewsItem &item = m_items.at(index.row());
switch (role) {
case IdRole: return item.id;
case TitleRole: return item.title;
case ContentRole: return item.content;
case TimestampRole: return item.timestamp.toString(Qt::ISODate);
case IsReadRole: return item.read;
case IsProcessedRole: return index.row() == m_processedIndex;
default: return QVariant();
}
}
QHash<int, QByteArray> NewsModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[IdRole] = "id";
roles[TitleRole] = "title";
roles[ContentRole] = "content";
roles[TimestampRole] = "timestamp";
roles[IsReadRole] = "read";
roles[IsProcessedRole] = "isProcessed";
return roles;
}
void NewsModel::markAsRead(int index)
{
if (index < 0 || index >= m_items.size())
return;
if (!m_items[index].read) {
m_items[index].read = true;
m_readIds.insert(m_items[index].id);
saveReadIds();
QModelIndex idx = createIndex(index, 0);
emit dataChanged(idx, idx, { IsReadRole });
emit hasUnreadChanged();
}
}
int NewsModel::processedIndex() const
{
return m_processedIndex;
}
void NewsModel::setProcessedIndex(int index)
{
if (index < 0 || index >= m_items.size() || m_processedIndex == index)
return;
m_processedIndex = index;
emit processedIndexChanged(index);
}
void NewsModel::updateModel(const QJsonArray &serverItems)
{
QList<NewsItem> updatedItems;
for (const QJsonValue &value : serverItems) {
if (!value.isObject())
continue;
QJsonObject object = value.toObject();
NewsItem item;
item.id = object.value("id").toString();
item.title = object.value("title").toString();
item.content = object.value("content").toString();
item.timestamp = QDateTime::fromString(object.value("timestamp").toString(), Qt::ISODate);
item.read = m_readIds.contains(object.value("id").toString());
updatedItems.append(item);
}
beginResetModel();
m_items = updatedItems;
std::sort(m_items.begin(), m_items.end(), [](const NewsItem &a, const NewsItem &b) { return a.timestamp > b.timestamp; });
endResetModel();
loadReadIds();
emit hasUnreadChanged();
}
bool NewsModel::hasUnread() const
{
for (const NewsItem &item : m_items) {
if (!item.read)
return true;
}
return false;
}
void NewsModel::loadReadIds()
{
QStringList ids = m_settings->readNewsIds();
m_readIds = QSet<QString>(ids.begin(), ids.end());
}
void NewsModel::saveReadIds() const
{
m_settings->setReadNewsIds(QStringList(m_readIds.begin(), m_readIds.end()));
}
+62
View File
@@ -0,0 +1,62 @@
#ifndef NEWSMODEL_H
#define NEWSMODEL_H
#include "settings.h"
#include <QAbstractListModel>
#include <QDateTime>
#include <QJsonArray>
#include <QSet>
#include <QString>
#include <QVector>
#include <memory>
struct NewsItem
{
QString id;
QString title;
QString content;
QDateTime timestamp;
bool read;
};
class NewsModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles {
IdRole = Qt::UserRole + 1,
TitleRole,
ContentRole,
TimestampRole,
IsReadRole,
IsProcessedRole
};
explicit NewsModel(const std::shared_ptr<Settings> &settings, QObject *parent = nullptr);
Q_INVOKABLE void markAsRead(int index);
Q_PROPERTY(int processedIndex READ processedIndex WRITE setProcessedIndex NOTIFY processedIndexChanged)
Q_PROPERTY(bool hasUnread READ hasUnread NOTIFY hasUnreadChanged)
int processedIndex() const;
void setProcessedIndex(int index);
void updateModel(const QJsonArray &items);
bool hasUnread() const;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override;
signals:
void processedIndexChanged(int index);
void hasUnreadChanged();
private:
QVector<NewsItem> m_items;
int m_processedIndex = -1;
std::shared_ptr<Settings> m_settings;
QSet<QString> m_readIds;
void loadReadIds();
void saveReadIds() const;
};
#endif // NEWSMODEL_H
+98 -1
View File
@@ -44,6 +44,8 @@ ServersModel::ServersModel(std::shared_ptr<Settings> settings, QObject *parent)
connect(this, &ServersModel::processedServerIndexChanged, this, &ServersModel::processedServerChanged);
connect(this, &ServersModel::dataChanged, this, &ServersModel::processedServerChanged);
connect(this, &QAbstractItemModel::modelReset, this, &ServersModel::recomputeGatewayStacks);
}
int ServersModel::rowCount(const QModelIndex &parent) const
@@ -156,6 +158,18 @@ QVariant ServersModel::data(const QModelIndex &index, int role) const
QString primaryDns = server.value(config_key::dns1).toString();
return primaryDns == protocols::dns::amneziaDnsIp;
}
case IsAdVisibleRole:{
return apiConfig.value(apiDefs::key::serviceInfo).toObject().value(apiDefs::key::isAdVisible).toBool(false);
}
case AdHeaderRole: {
return apiConfig.value(apiDefs::key::serviceInfo).toObject().value(apiDefs::key::adHeader).toString();
}
case AdDescriptionRole: {
return apiConfig.value(apiDefs::key::serviceInfo).toObject().value(apiDefs::key::adDescription).toString();
}
case AdEndpointRole: {
return apiConfig.value(apiDefs::key::serviceInfo).toObject().value(apiDefs::key::adEndpoint).toString();
}
}
return QVariant();
@@ -173,6 +187,7 @@ void ServersModel::resetModel()
m_servers = m_settings->serversArray();
m_defaultServerIndex = m_settings->defaultServerIndex();
m_processedServerIndex = m_defaultServerIndex;
m_isAmneziaDnsEnabled = m_settings->useAmneziaDns();
endResetModel();
emit defaultServerIndexChanged(m_defaultServerIndex);
}
@@ -374,7 +389,6 @@ QHash<int, QByteArray> ServersModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[NameRole] = "serverName";
roles[NameRole] = "name";
roles[ServerDescriptionRole] = "serverDescription";
roles[CollapsedServerDescriptionRole] = "collapsedServerDescription";
@@ -401,6 +415,12 @@ QHash<int, QByteArray> ServersModel::roleNames() const
roles[IsCountrySelectionAvailableRole] = "isCountrySelectionAvailable";
roles[ApiAvailableCountriesRole] = "apiAvailableCountries";
roles[ApiServerCountryCodeRole] = "apiServerCountryCode";
roles[IsAdVisibleRole] = "isAdVisible";
roles[AdHeaderRole] = "adHeader";
roles[AdDescriptionRole] = "adDescription";
roles[AdEndpointRole] = "adEndpoint";
return roles;
}
@@ -755,6 +775,68 @@ bool ServersModel::isServerFromApi(const int serverIndex)
return data(serverIndex, IsServerFromTelegramApiRole).toBool() || data(serverIndex, IsServerFromGatewayApiRole).toBool();
}
bool ServersModel::hasServersFromGatewayApi()
{
return !m_gatewayStacks.isEmpty();
}
bool ServersModel::GatewayStacks::operator==(const GatewayStacks &other) const
{
return userCountryCodes == other.userCountryCodes && serviceTypes == other.serviceTypes;
}
QJsonObject ServersModel::GatewayStacks::toJson() const
{
QJsonObject obj;
if (!userCountryCodes.isEmpty()) {
obj.insert(configKey::userCountryCode, QJsonArray::fromStringList(userCountryCodes.values()));
}
if (!serviceTypes.isEmpty()) {
obj.insert(configKey::serviceType, QJsonArray::fromStringList(serviceTypes.values()));
}
return obj;
}
void ServersModel::recomputeGatewayStacks()
{
const bool wasEmpty = m_gatewayStacks.isEmpty();
GatewayStacks computed;
bool hasNewTags = false;
for (int i = 0; i < m_servers.count(); ++i) {
if (data(i, IsServerFromGatewayApiRole).toBool()) {
const QJsonObject server = m_servers.at(i).toObject();
const QJsonObject apiConfig = server.value(configKey::apiConfig).toObject();
const QString userCountryCode = apiConfig.value(configKey::userCountryCode).toString();
const QString serviceType = apiConfig.value(configKey::serviceType).toString();
if (!userCountryCode.isEmpty()) {
if (!m_gatewayStacks.userCountryCodes.contains(userCountryCode)) {
hasNewTags = true;
}
computed.userCountryCodes.insert(userCountryCode);
}
if (!serviceType.isEmpty()) {
if (!m_gatewayStacks.serviceTypes.contains(serviceType)) {
hasNewTags = true;
}
computed.serviceTypes.insert(serviceType);
}
}
}
m_gatewayStacks = std::move(computed);
if (hasNewTags) {
emit gatewayStacksExpanded();
}
if (wasEmpty != m_gatewayStacks.isEmpty()) {
emit hasServersFromGatewayApiChanged();
}
}
bool ServersModel::isApiKeyExpired(const int serverIndex)
{
auto serverConfig = m_servers.at(serverIndex).toObject();
@@ -821,3 +903,18 @@ bool ServersModel::processedServerIsPremium() const
{
return apiUtils::isPremiumServer(getServerConfig(m_processedServerIndex));
}
bool ServersModel::isAdVisible()
{
return data(m_defaultServerIndex, IsAdVisibleRole).toBool();
}
QString ServersModel::adHeader()
{
return data(m_defaultServerIndex, AdHeaderRole).toString();
}
QString ServersModel::adDescription()
{
return data(m_defaultServerIndex, AdDescriptionRole).toString();
}
+34
View File
@@ -10,6 +10,16 @@ class ServersModel : public QAbstractListModel
{
Q_OBJECT
public:
struct GatewayStacks
{
QSet<QString> userCountryCodes;
QSet<QString> serviceTypes;
bool isEmpty() const { return userCountryCodes.isEmpty() && serviceTypes.isEmpty(); }
bool operator==(const GatewayStacks &other) const;
QJsonObject toJson() const;
};
enum Roles {
NameRole = Qt::UserRole + 1,
ServerDescriptionRole,
@@ -37,6 +47,10 @@ public:
IsCountrySelectionAvailableRole,
ApiAvailableCountriesRole,
ApiServerCountryCodeRole,
IsAdVisibleRole,
AdHeaderRole,
AdDescriptionRole,
AdEndpointRole,
HasAmneziaDns
};
@@ -52,6 +66,8 @@ public:
void resetModel();
GatewayStacks gatewayStacks() const { return m_gatewayStacks; }
Q_PROPERTY(int defaultIndex READ getDefaultServerIndex WRITE setDefaultServerIndex NOTIFY defaultServerIndexChanged)
Q_PROPERTY(QString defaultServerName READ getDefaultServerName NOTIFY defaultServerNameChanged)
Q_PROPERTY(QString defaultServerDefaultContainerName READ getDefaultServerDefaultContainerName NOTIFY defaultServerDefaultContainerChanged)
@@ -62,9 +78,15 @@ public:
defaultServerDefaultContainerChanged)
Q_PROPERTY(bool isDefaultServerFromApi READ isDefaultServerFromApi NOTIFY defaultServerIndexChanged)
Q_PROPERTY(bool hasServersFromGatewayApi READ hasServersFromGatewayApi NOTIFY hasServersFromGatewayApiChanged)
Q_PROPERTY(int processedIndex READ getProcessedServerIndex WRITE setProcessedServerIndex NOTIFY processedServerIndexChanged)
Q_PROPERTY(bool processedServerIsPremium READ processedServerIsPremium NOTIFY processedServerChanged)
Q_PROPERTY(bool isAdVisible READ isAdVisible NOTIFY defaultServerIndexChanged)
Q_PROPERTY(QString adHeader READ adHeader NOTIFY defaultServerIndexChanged)
Q_PROPERTY(QString adDescription READ adDescription NOTIFY defaultServerIndexChanged)
bool processedServerIsPremium() const;
public slots:
@@ -82,6 +104,8 @@ public slots:
bool isDefaultServerHasWriteAccess();
bool hasServerWithWriteAccess();
bool hasServersFromGatewayApi();
const int getServersCount();
void setProcessedServerIndex(const int index);
@@ -128,6 +152,10 @@ public slots:
bool isApiKeyExpired(const int serverIndex);
void removeApiConfig(const int serverIndex);
bool isAdVisible();
QString adHeader();
QString adDescription();
protected:
QHash<int, QByteArray> roleNames() const override;
@@ -147,6 +175,9 @@ signals:
void updateApiCountryModel();
void updateApiServicesModel();
void hasServersFromGatewayApiChanged();
void gatewayStacksExpanded();
private:
ServerCredentials serverCredentials(int index) const;
@@ -167,6 +198,9 @@ private:
int m_processedServerIndex;
bool m_isAmneziaDnsEnabled = m_settings->useAmneziaDns();
GatewayStacks m_gatewayStacks;
void recomputeGatewayStacks();
};
#endif // SERVERSMODEL_H
+113 -36
View File
@@ -2,7 +2,6 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Shapes
import Qt5Compat.GraphicalEffects
import Style 1.0
@@ -13,61 +12,139 @@ import "../Controls2/TextTypes"
Rectangle {
id: root
property real contentHeight: ad.implicitHeight + ad.anchors.topMargin + ad.anchors.bottomMargin
property real contentHeight: content.implicitHeight + content.anchors.topMargin + content.anchors.bottomMargin
property bool isFocusable: true
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop { position: 0.0; color: AmneziaStyle.color.translucentSlateGray }
GradientStop { position: 1.0; color: AmneziaStyle.color.translucentOnyxBlack }
}
border.width: 1
border.color: AmneziaStyle.color.goldenApricot
color: AmneziaStyle.color.transparent
border.color: AmneziaStyle.color.onyxBlack
radius: 13
visible: false
// visible: GC.isDesktop() && ServersModel.isDefaultServerFromApi
// && ServersModel.isDefaultServerDefaultContainerHasSplitTunneling && SettingsController.isHomeAdLabelVisible
visible: ServersModel.isAdVisible
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
Keys.onTabPressed: {
FocusController.nextKeyTabItem()
}
onClicked: function() {
Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl("premium"))
}
Keys.onBacktabPressed: {
FocusController.previousKeyTabItem()
}
Keys.onUpPressed: {
FocusController.nextKeyUpItem()
}
Keys.onDownPressed: {
FocusController.nextKeyDownItem()
}
Keys.onLeftPressed: {
FocusController.nextKeyLeftItem()
}
Keys.onRightPressed: {
FocusController.nextKeyRightItem()
}
Keys.onEnterPressed: {
Qt.openUrlExternally(ServersModel.getDefaultServerData("adEndpoint"))
}
Keys.onReturnPressed: {
Qt.openUrlExternally(ServersModel.getDefaultServerData("adEndpoint"))
}
RowLayout {
id: ad
id: content
anchors.fill: parent
anchors.margins: 16
anchors.leftMargin: 16
anchors.rightMargin: 12
anchors.topMargin: 12
anchors.bottomMargin: 12
spacing: 20
Image {
source: "qrc:/images/controls/amnezia.svg"
sourceSize: Qt.size(36, 36)
ColumnLayout {
Layout.fillWidth: true
spacing: 4
layer {
effect: ColorOverlay {
color: AmneziaStyle.color.paleGray
}
CaptionTextType {
Layout.fillWidth: true
text: ServersModel.adHeader
color: AmneziaStyle.color.paleGray
font.pixelSize: 14
font.weight: 700
textFormat: Text.RichText
}
CaptionTextType {
Layout.fillWidth: true
text: ServersModel.adDescription
color: AmneziaStyle.color.mutedGray
wrapMode: Text.WordWrap
lineHeight: 18
lineHeightMode: Text.FixedHeight
font.pixelSize: 14
visible: text !== ""
}
}
CaptionTextType {
Layout.fillWidth: true
Layout.rightMargin: 10
Layout.leftMargin: 10
Item {
implicitWidth: 40
implicitHeight: 40
Layout.alignment: Qt.AlignVCenter
text: qsTr("Amnezia Premium - for access to all websites and online resources")
color: AmneziaStyle.color.pearlGray
Rectangle {
id: chevronBackground
anchors.fill: parent
radius: 12
color: AmneziaStyle.color.transparent
border.width: root.activeFocus ? 1 : 0
border.color: AmneziaStyle.color.paleGray
lineHeight: 18
font.pixelSize: 15
}
Behavior on color {
PropertyAnimation { duration: 200 }
}
ImageButtonType {
image: "qrc:/images/controls/close.svg"
imageColor: AmneziaStyle.color.paleGray
Behavior on border.width {
PropertyAnimation { duration: 200 }
}
}
onClicked: function() {
SettingsController.disableHomeAdLabel()
Image {
anchors.centerIn: parent
source: "qrc:/images/controls/chevron-right.svg"
sourceSize: Qt.size(24, 24)
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onEntered: {
chevronBackground.color = AmneziaStyle.color.slateGray
}
onExited: {
chevronBackground.color = AmneziaStyle.color.transparent
}
onPressedChanged: {
chevronBackground.color = pressed ? AmneziaStyle.color.charcoalGray : containsMouse ? AmneziaStyle.color.slateGray : AmneziaStyle.color.transparent
}
onClicked: function() {
root.forceActiveFocus()
Qt.openUrlExternally(ServersModel.getDefaultServerData("adEndpoint"))
}
}
}
+1 -1
View File
@@ -32,7 +32,7 @@ DrawerType2 {
spacing: 8
onImplicitHeightChanged: {
root.expandedHeight = content.implicitHeight + 32
root.expandedHeight = content.implicitHeight + 32 + SettingsController.safeAreaBottomMargin
}
Header2TextType {

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