Compare commits

...

51 Commits

Author SHA1 Message Date
NickVs2015
577c9b83b5 fix: linux firewall improvements
- Add xargs -r to avoid spurious delete when no duplicate chain entries exist
- Pre-register routing table name in /etc/iproute2/rt_tables
- Mount legacy net_cls cgroup v1 on cgroup v2 (unified hierarchy) systems
- Add 400.allowPIA anchor during install
- Ignore errors on ip route flush for table that may not exist yet
- Remove rt_tables entry on teardown
2026-04-01 01:21:48 +03:00
NickVs2015
bf3d11e5c4 feat: renewal new status logic (#2409)
* fix: renewal add status logic

* fix: wakeup activity resumed android
2026-03-25 19:48:32 +08:00
NickVs2015
9a0222aee3 fix: ui fixes for renewal subscription (#2406) 2026-03-25 12:34:42 +08:00
NickVs2015
f0f0f7c5be feat: add subscription renewal (#2389)
* feat: add renewal subsribe

* fix: after review
2026-03-24 22:45:02 +08:00
NickVs2015
36b1a863bf fix: black screen resume / pause (#2400) 2026-03-24 22:13:31 +08:00
yyy-amnezia
4103c5bbcf refactor: extract and simplify OpenVPN reachability and network change handling logic (#2402) 2026-03-24 22:12:59 +08:00
vkamn
fa69da6d56 chore: send app version in services request (#2403) 2026-03-24 20:25:04 +08:00
yyy-amnezia
aaf2c9ddeb feat: add Xray split tunnel support for iOS PacketTunnelProvider (#2332) 2026-03-24 16:07:36 +08:00
Mitternacht822
dbbc7119ec feat: add warning info for ssh keys (#2252)
* fix: fixed da typo

* feat: added warning about available ssh keys info
2026-03-24 16:06:40 +08:00
vkamn
c57162c4cc feat: add base amnezia trial support (#2366)
* feat: add base amnezia trial support

* feat: add external-trial
2026-03-24 10:29:51 +08:00
NickVs2015
40e39895c9 fix openfile deadlock (#2373) 2026-03-21 11:46:46 +08:00
vkamn
ec3ab2a03c chore: update licnese file (#2376) 2026-03-20 21:04:13 +08:00
yyy-amnezia
ddecfcad26 fix: apple platform network switch fix (#2359)
* Apple platform network switch fix

* macos_ne exclusion fixed
2026-03-20 20:51:36 +08:00
NickVs2015
67bd880cdf fix: swap buffers error (#2347) 2026-03-16 13:03:20 +08:00
vkamn
477afb9d85 chore: bump version (#2336) 2026-03-10 22:22:37 +08:00
NickVs2015
f969fcdbb8 fix: restore dpad functionality ATV (#2335) 2026-03-10 22:19:55 +08:00
vkamn
b0ca16d861 chore: bump version (#2331) 2026-03-09 18:29:56 +08:00
NickVs2015
9963359948 fix: disable gamepad for GP (#2321) 2026-03-09 17:39:50 +08:00
vkamn
ca639d293d chore: bump version (#2319) 2026-03-06 23:11:03 +08:00
NickVs2015
83d045af64 fix: GP requrements (#2312) 2026-03-06 17:05:16 +08:00
NickVs2015
aea8ff4961 fix: add handle handleContextCreationFailure (#2309) 2026-03-03 22:04:45 +08:00
vkamn
1892db4375 fix: remove nested qeventloop from isConfigValid (also rename to validateConfig) (#2305)
* fix: remove nested qeventloop from isConfigValid (also rename to validateConfig)

* chore: bump version
2026-03-03 20:58:32 +08:00
NickVs2015
c86a641e05 fix: add suppord android 9 gamepad and remote control (#2302) 2026-03-03 15:14:51 +08:00
vkamn
befb2bf19a chore: bump version (#2295) 2026-02-27 23:33:37 +08:00
vkamn
7ad6bc340c chore: add translations for ru (#2285)
* chore: add translations for ru

* chore: text fixes
2026-02-27 20:00:31 +08:00
vkamn
9164e38c34 fix: restore backup android (#2291)
* fix: fixed restore backup on android

* chore: add resume helper for android

* chore: add ResumeHelper.runWhenActive call after all native android dialogs

* fix: add permission for tv file picker

* fix: add file picker handler in kotlin

---------

Co-authored-by: NickVs2015 <nv@amnezia.org>
2026-02-27 18:43:36 +08:00
vkamn
8f7559f01b chore: revert PR #2222 (#2290) 2026-02-27 14:29:25 +08:00
vkamn
af56200735 fix: fixed adding s3 s4 when updating the server conf for awg lagacy (#2289) 2026-02-27 14:11:40 +08:00
vkamn
3874050fae fix: again fixed s3, s4 ranges (#2288) 2026-02-27 13:37:49 +08:00
vkamn
3087163e34 fix: fixed s3, s4 ranges (#2283) 2026-02-26 22:31:41 +08:00
Mitternacht822
1fa152845c fix: generate native awg config as qr series (#2221) 2026-02-26 22:31:18 +08:00
vkamn
50e23ef233 fix: awg config update (#2281)
* fix: fixed client config update for awg container

* chore: bump version
2026-02-26 22:12:58 +08:00
Yaroslav Gurov
ea648466de chore: remove redundant VpnConnection usage from SitesController (#2278) 2026-02-26 17:55:08 +08:00
Yaroslav Gurov
b782775016 fix: change event looping to mutexes for settings and secureqsettings (#2270) 2026-02-26 11:41:08 +08:00
NickVs2015
89a7fe1081 fix: fixed remote control for ATV (#2277) 2026-02-26 11:40:16 +08:00
Yaroslav Gurov
e8bb096025 fix: ios wrong awg blob (#2272) 2026-02-24 17:56:17 +07:00
Mitternacht822
fd5c7c8322 fix: copy LICENSE to build as LICENSE.txt for WiX CPack (#2265)
* fix(installer): copy LICENSE to build as LICENSE.txt for WiX CPack

* fix: fixed a typo

* fix: fixed a typo
2026-02-24 14:07:48 +08:00
Yaroslav Gurov
e798d0f503 feat: update amneziawg-android dependencies (#2269) 2026-02-24 00:54:55 +08:00
Yaroslav Gurov
bbb0abb596 feat: update xray (#2267) 2026-02-24 00:27:29 +08:00
vkamn
0925aec86a chore: bump version (#2264) 2026-02-23 18:01:59 +08:00
Yaroslav Gurov
b084c4c284 fix: ios connection status stuck (#2263) 2026-02-23 18:00:13 +08:00
vkamn
87288ebccd chore: bump version (#2262) 2026-02-23 17:16:24 +08:00
vkamn
fcd7eadf4c chore: bump version (#2261) 2026-02-23 15:38:27 +08:00
Mitternacht822
0373338fb7 fix: randomized baseUrls traversal order in GatewayController::getProxyUrls (#2247) 2026-02-23 15:33:35 +08:00
Yaroslav Gurov
42f070fe9d fix: handle Android disconnected status properly (#2255) 2026-02-23 15:31:15 +08:00
Mitternacht822
02be6dc5f9 chore: add license to msi installer (#2227) 2026-02-20 12:13:08 +08:00
vkamn
bfcf7f0305 chore: bump version (#2244) 2026-02-19 20:27:42 +08:00
Mitternacht822
2bce595ade fix: remove revoke from remove subscription flow (#2226)
* fix(revoke): now revoke calls only for unlink device action

* fix: removed revoke call when removing a subscription from the app
2026-02-19 20:23:13 +08:00
Yaroslav Gurov
cd1e561fd4 fix: add network watcher back (#2240)
* feat: add reconnect in case of changing network

* fix: reconnect to VPN on wakeup

* fix: linux wakeup build
2026-02-19 20:21:49 +08:00
Mitternacht822
9bd1e6a0f5 fix: added stop and delete AmneziaVPNSplitTunnel on uninstall (#2222) 2026-02-18 11:21:59 +08:00
Yaroslav Gurov
5058c9aa6f fix: do not enable killswitch by default when service starts (#2232) 2026-02-18 10:59:16 +08:00
74 changed files with 1876 additions and 1010 deletions

View File

@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
set(PROJECT AmneziaVPN)
set(AMNEZIAVPN_VERSION 4.8.13.0)
set(AMNEZIAVPN_VERSION 4.8.14.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 2106)
set(APP_ANDROID_VERSION_CODE 2118)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux")
@@ -61,6 +61,9 @@ if(WIN32 AND NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
set(CPACK_PACKAGE_VENDOR "AmneziaVPN")
set(CPACK_PACKAGE_VERSION ${AMNEZIAVPN_VERSION})
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "AmneziaVPN client")
set(AMNEZIA_LICENSE_TXT "${CMAKE_BINARY_DIR}/LICENSE.txt")
configure_file("${CMAKE_SOURCE_DIR}/LICENSE" "${AMNEZIA_LICENSE_TXT}" COPYONLY)
set(CPACK_RESOURCE_FILE_LICENSE "${AMNEZIA_LICENSE_TXT}")
set(CPACK_PACKAGE_INSTALL_DIRECTORY "AmneziaVPN")
set(CPACK_PACKAGE_DIRECTORY "${CMAKE_BINARY_DIR}")
set(CPACK_PACKAGE_EXECUTABLES "AmneziaVPN" "AmneziaVPN")

View File

@@ -179,7 +179,7 @@ You may face compiling issues in QT Creator after you've worked in Android Studi
## License
GPL v3.0
This project is licensed under the GNU General Public License v3.0 (see LICENSE) and also includes third-party components distributed under their own terms (see THIRD_PARTY_LICENSES.md).
## Donate

149
THIRD_PARTY_LICENSES.md Normal file
View File

@@ -0,0 +1,149 @@
# Third-Party Licenses
This project is licensed under the GNU General Public License v3.0.
This file lists third-party software components used by this repository.
Each component is distributed under its own license as linked below.
---
## QtKeychain
- Source: https://github.com/frankosterfeld/qtkeychain
- License: BSD License
- License Text: https://www.gnu.org/licenses/license-list.html#ModifiedBSD
---
## QSimpleCrypto
- Source: https://github.com/n1flh31mur/QSimpleCrypto
- License: Apache License 2.0
- License Text: https://github.com/n1flh31mur/QSimpleCrypto/blob/master/LICENSE
---
## SortFilterProxyModel
- Source: https://github.com/oKcerG/SortFilterProxyModel
- License: MIT License
- License Text: https://github.com/oKcerG/SortFilterProxyModel/blob/master/LICENSE
---
## QJsonStruct
- Source: https://github.com/Qv2ray/QJsonStruct
- License: MIT License
- License Text: https://github.com/Qv2ray/QJsonStruct/blob/master/LICENSE
---
## QR Code Generator (qrcodegen)
- Source: https://github.com/nayuki/QR-Code-generator
- License: MIT License
- License Text: https://www.nayuki.io/page/qr-code-generator-library
---
## Qt Gamepad
- Source: https://github.com/qt/qtgamepad
- License: GNU General Public License v3.0 (GPL-3.0)
- License Text: https://www.gnu.org/licenses/gpl-3.0.en.html
---
## AmneziaWG Apple (WireGuard)
- Source: https://github.com/amnezia-vpn/amneziawg-apple
- License: MIT License
- License Text: https://github.com/amnezia-vpn/amneziawg-apple/blob/master/COPYING
---
## AmneziaWG Android
- Source: https://github.com/amnezia-vpn/amneziawg-go
- License: MIT License
- License Text: https://github.com/amnezia-vpn/amneziawg-go/blob/master/LICENSE
---
## Xray Core
- Source: https://github.com/XTLS/Xray-core
- License: Mozilla Public License 2.0 (MPL-2.0)
- License Text: https://github.com/XTLS/Xray-core/blob/main/LICENSE
---
## Cloak
- Source: https://github.com/cbeuw/Cloak
- License: GNU General Public License v3.0 (GPL-3.0)
- License Text: https://github.com/cbeuw/Cloak/blob/master/LICENSE
---
## Shadowsocks
- Source: https://github.com/shadowsocks/shadowsocks-libev
- License: GPL-3.0-or-later
- License Text: http://www.gnu.org/licenses/
---
## OpenSSL
- Source: https://github.com/openssl/openssl
- License: Apache License 2.0
- License Text: https://www.openssl.org/source/license.html
---
## libssh
- Source: https://www.libssh.org/
- License: GNU Lesser General Public License (LGPL)
- License Text: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
---
## OpenVPNAdapter
- Source: https://github.com/ss-abramchuk/OpenVPNAdapter
- License: GNU Affero General Public License v3.0 (AGPL-3.0)
- License Text: https://github.com/ss-abramchuk/OpenVPNAdapter/blob/master/LICENSE
---
## Wintun
- Source: https://www.wintun.net/
- License: Prebuilt Binaries License
- License Text: https://github.com/WireGuard/wintun/blob/master/prebuilt-binaries-license.txt
---
## Mullvad Split Tunnel Driver
- Source: https://github.com/mullvad/win-split-tunnel
- License: GNU General Public License v3.0 (GPL-3.0) and Mozilla Public License Version 2.0
- License Text: https://github.com/mullvad/win-split-tunnel/blob/master/LICENSE-GPL.md https://github.com/mullvad/win-split-tunnel/blob/master/LICENSE-MPL.txt
---
## tun2socks
- Source: https://github.com/eycorsican/go-tun2socks
- License: MIT License
- License Text: https://github.com/eycorsican/go-tun2socks/blob/master/LICENSE
---
## TAP-Windows Driver
- Source: https://github.com/OpenVPN/tap-windows6
- License: tap-windows6 license
- License Text: https://github.com/OpenVPN/tap-windows6/blob/master/COPYING

View File

@@ -78,6 +78,7 @@ set(AMNEZIAVPN_TS_FILES
)
file(GLOB_RECURSE AMNEZIAVPN_TS_SOURCES *.qrc *.cpp *.h *.ui)
list(FILTER AMNEZIAVPN_TS_SOURCES EXCLUDE REGEX "qtgamepad/examples")
qt_create_translation(AMNEZIAVPN_QM_FILES ${AMNEZIAVPN_TS_SOURCES} ${AMNEZIAVPN_TS_FILES})

View File

@@ -109,6 +109,16 @@ void AmneziaApplication::init()
// install filter on main window
if (auto win = qobject_cast<QQuickWindow*>(obj)) {
win->installEventFilter(this);
#ifdef Q_OS_ANDROID
QObject::connect(win, &QQuickWindow::sceneGraphError,
[](QQuickWindow::SceneGraphError, const QString &msg) {
qWarning() << "Scene graph error (suppressed):" << msg;
});
// Keep graphics context alive across hide/show cycles to avoid
// eglSwapBuffers/makeCurrent being called on a context Android has reclaimed.
win->setPersistentSceneGraph(true);
win->setPersistentGraphics(true);
#endif
win->show();
}
},

View File

@@ -75,6 +75,8 @@ private const val OPEN_FILE_ACTION_CODE = 3
private const val CHECK_NOTIFICATION_PERMISSION_ACTION_CODE = 4
private const val PREFS_NOTIFICATION_PERMISSION_ASKED = "NOTIFICATION_PERMISSION_ASKED"
private const val OPEN_FILE_AFTER_RESUME_DELAY_MS = 400L
private const val KEY_PENDING_OPEN_FILE_URI = "pending_open_file_uri"
class AmneziaActivity : QtActivity() {
@@ -90,10 +92,12 @@ class AmneziaActivity : QtActivity() {
private val actionResultHandlers = mutableMapOf<Int, ActivityResultHandler>()
private val permissionRequestHandlers = mutableMapOf<Int, PermissionRequestHandler>()
private var isActivityResumed = false
private var hasWindowFocus = false
private val resumeHandler = Handler(Looper.getMainLooper())
private var pendingOpenFileUri: String? = null
private var openFileDeliveryScheduled = false
private val vpnServiceEventHandler: Handler by lazy(NONE) {
object : Handler(Looper.getMainLooper()) {
@@ -196,11 +200,18 @@ class AmneziaActivity : QtActivity() {
doBindService()
}
)
pendingOpenFileUri = savedInstanceState?.getString(KEY_PENDING_OPEN_FILE_URI)
openFileDeliveryScheduled = false
registerBroadcastReceivers()
intent?.let(::processIntent)
runBlocking { vpnProto = proto.await() }
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
pendingOpenFileUri?.let { outState.putString(KEY_PENDING_OPEN_FILE_URI, it) }
}
private fun loadLibs() {
listOf(
"rsapss",
@@ -270,6 +281,7 @@ class AmneziaActivity : QtActivity() {
hasWindowFocus = false
// Cancel all pending operations when activity stops
resumeHandler.removeCallbacksAndMessages(null)
openFileDeliveryScheduled = false
Log.d(TAG, "Stop Amnezia activity")
doUnbindService()
mainScope.launch {
@@ -283,43 +295,55 @@ class AmneziaActivity : QtActivity() {
super.onWindowFocusChanged(hasFocus)
hasWindowFocus = hasFocus
Log.d(TAG, "Window focus changed: hasFocus=$hasFocus")
// Cancel pending operations if window loses focus
if (!hasFocus) {
// Cancel pending operations if window loses focus
resumeHandler.removeCallbacksAndMessages(null)
} else if (isActivityResumed && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
window.decorView.apply {
invalidate()
resumeHandler.postDelayed({
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
sendTouch(1f, 1f)
}
}, 50)
resumeHandler.postDelayed({
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
sendTouch(2f, 2f)
requestLayout()
invalidate()
}
}, 150)
}
}
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
val deviceId = event.deviceId
val keyCode = event.keyCode
val pressed = event.action == KeyEvent.ACTION_DOWN
val source = event.source
if (deviceId < 0 && pressed) {
when (keyCode) {
KeyEvent.KEYCODE_BUTTON_A,
KeyEvent.KEYCODE_BUTTON_B,
KeyEvent.KEYCODE_BUTTON_X,
KeyEvent.KEYCODE_BUTTON_Y,
KeyEvent.KEYCODE_BUTTON_START,
KeyEvent.KEYCODE_BUTTON_SELECT,
KeyEvent.KEYCODE_DPAD_CENTER -> {
nativeGamepadKeyEvent(0, keyCode, true)
nativeGamepadKeyEvent(0, keyCode, false)
when (keyCode) {
KeyEvent.KEYCODE_BUTTON_A,
KeyEvent.KEYCODE_BUTTON_B,
KeyEvent.KEYCODE_BUTTON_X,
KeyEvent.KEYCODE_BUTTON_Y,
KeyEvent.KEYCODE_BUTTON_START,
KeyEvent.KEYCODE_BUTTON_SELECT -> {
nativeGamepadKeyEvent(0, keyCode, pressed)
return true
}
}
}
// Real gamepad events (deviceId >= 0)
if (deviceId >= 0) {
val isGamepad = (source and InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD
val isJoystick = (source and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK
val isDpad = (source and InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD
if (isGamepad || isJoystick || isDpad) {
nativeGamepadKeyEvent(deviceId, keyCode, pressed)
return true
KeyEvent.KEYCODE_DPAD_CENTER,
KeyEvent.KEYCODE_DPAD_UP,
KeyEvent.KEYCODE_DPAD_DOWN,
KeyEvent.KEYCODE_DPAD_LEFT,
KeyEvent.KEYCODE_DPAD_RIGHT -> {
val syntheticKeyCode = if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) KeyEvent.KEYCODE_ENTER else keyCode
val synthetic = KeyEvent(
event.downTime, event.eventTime, event.action, syntheticKeyCode,
event.repeatCount, event.metaState, -1, event.scanCode,
event.flags, InputDevice.SOURCE_KEYBOARD
)
return super.dispatchKeyEvent(synthetic)
}
}
@@ -329,10 +353,18 @@ class AmneziaActivity : QtActivity() {
private external fun nativeGamepadKeyEvent(deviceId: Int, keyCode: Int, pressed: Boolean)
override fun onPause() {
// Notify Qt to stop rendering BEFORE super.onPause() destroys the EGL surface.
// Using a coroutine here would be too late — the surface is gone by the time
// the coroutine runs. A direct synchronous call gives Qt's render thread the
// best chance to process visible=false before surface destruction.
if (qtInitialized.isCompleted) {
QtAndroidController.onActivityPaused()
}
super.onPause()
isActivityResumed = false
// Cancel all pending operations when activity pauses
resumeHandler.removeCallbacksAndMessages(null)
openFileDeliveryScheduled = false
Log.d(TAG, "Pause Amnezia activity")
}
@@ -340,6 +372,24 @@ class AmneziaActivity : QtActivity() {
super.onResume()
isActivityResumed = true
Log.d(TAG, "Resume Amnezia activity")
if (qtInitialized.isCompleted) {
QtAndroidController.onActivityResumed()
}
if (pendingOpenFileUri != null && !openFileDeliveryScheduled) {
val uri = pendingOpenFileUri!!
openFileDeliveryScheduled = true
resumeHandler.postDelayed({
if (!isFinishing && !isDestroyed) {
pendingOpenFileUri = null
openFileDeliveryScheduled = false
mainScope.launch {
qtInitialized.await()
QtAndroidController.onFileOpened(uri)
}
}
}, OPEN_FILE_AFTER_RESUME_DELAY_MS)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
window.decorView.apply {
@@ -351,13 +401,13 @@ class AmneziaActivity : QtActivity() {
sendTouch(1f, 1f)
}
}, 100)
resumeHandler.postDelayed({
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
sendTouch(2f, 2f)
}
}, 200)
resumeHandler.postDelayed({
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
requestLayout()
@@ -403,25 +453,25 @@ class AmneziaActivity : QtActivity() {
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
}
@@ -757,9 +807,13 @@ class AmneziaActivity : QtActivity() {
grantUriPermission(packageName, this, Intent.FLAG_GRANT_READ_URI_PERMISSION)
}?.toString() ?: ""
Log.v(TAG, "Open file: $uri")
mainScope.launch {
qtInitialized.await()
QtAndroidController.onFileOpened(uri)
if (uri.isNotEmpty()) {
pendingOpenFileUri = uri
} else {
mainScope.launch {
qtInitialized.await()
QtAndroidController.onFileOpened(uri)
}
}
}
))
@@ -788,7 +842,7 @@ class AmneziaActivity : QtActivity() {
@Suppress("unused")
fun getFd(fileName: String): Int {
Log.v(TAG, "Get fd for $fileName")
return blockingCall {
return blockingCall(Dispatchers.IO) {
try {
pfd = contentResolver.openFileDescriptor(Uri.parse(fileName), "r")
pfd?.fd ?: -1

View File

@@ -33,7 +33,10 @@ class TvFilePicker : ComponentActivity() {
return intent
}
}) {
setResult(RESULT_OK, Intent().apply { data = it })
setResult(RESULT_OK, Intent().apply {
data = it
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
})
finish()
}

View File

@@ -31,4 +31,7 @@ object QtAndroidController {
external fun onImeInsetsChanged(heightDp: Int)
external fun onSystemBarsInsetsChanged(navBarHeightDp: Int, statusBarHeightDp: Int)
external fun onActivityPaused()
external fun onActivityResumed()
}

View File

@@ -163,7 +163,7 @@ add_custom_command(TARGET ${PROJECT} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory
$<TARGET_BUNDLE_DIR:AmneziaVPN>/Contents/Frameworks
COMMAND /usr/bin/find "$<TARGET_BUNDLE_DIR:AmneziaVPN>/Contents/Frameworks/OpenVPNAdapter.framework" -name "*.sha256" -delete
COMMAND /usr/bin/codesign --force --sign "Apple Distribution"
COMMAND /usr/bin/codesign --force --sign "Apple Distribution: Privacy Technologies OU"
"$<TARGET_BUNDLE_DIR:AmneziaVPN>/Contents/Frameworks/OpenVPNAdapter.framework/Versions/Current/OpenVPNAdapter"
COMMAND ${QT_BIN_DIR_DETECTED}/macdeployqt $<TARGET_BUNDLE_DIR:AmneziaVPN> -appstore-compliant -qmldir=${CMAKE_CURRENT_SOURCE_DIR}
COMMENT "Signing OpenVPNAdapter framework"

View File

@@ -10,8 +10,10 @@ namespace apiDefs
AmneziaFreeV3,
AmneziaPremiumV1,
AmneziaPremiumV2,
AmneziaTrialV2,
SelfHosted,
ExternalPremium
ExternalPremium,
ExternalTrial
};
enum ConfigSource {
@@ -32,6 +34,7 @@ namespace apiDefs
constexpr QLatin1String stackType("stack_type");
constexpr QLatin1String serviceType("service_type");
constexpr QLatin1String cliVersion("cli_version");
constexpr QLatin1String cliName("cli_name");
constexpr QLatin1String supportedProtocols("supported_protocols");
constexpr QLatin1String vpnKey("vpn_key");

View File

@@ -58,18 +58,24 @@ apiDefs::ConfigType apiUtils::getConfigType(const QJsonObject &serverConfigObjec
};
case apiDefs::ConfigSource::AmneziaGateway: {
constexpr QLatin1String servicePremium("amnezia-premium");
constexpr QLatin1String serviceTrial("amnezia-trial");
constexpr QLatin1String serviceFree("amnezia-free");
constexpr QLatin1String serviceExternalPremium("external-premium");
constexpr QLatin1String serviceExternalTrial("external-trial");
auto apiConfigObject = serverConfigObject.value(apiDefs::key::apiConfig).toObject();
auto serviceType = apiConfigObject.value(apiDefs::key::serviceType).toString();
if (serviceType == servicePremium) {
return apiDefs::ConfigType::AmneziaPremiumV2;
} else if (serviceType == serviceTrial) {
return apiDefs::ConfigType::AmneziaTrialV2;
} else if (serviceType == serviceFree) {
return apiDefs::ConfigType::AmneziaFreeV3;
} else if (serviceType == serviceExternalPremium) {
return apiDefs::ConfigType::ExternalPremium;
} else if (serviceType == serviceExternalTrial) {
return apiDefs::ConfigType::ExternalTrial;
}
}
default: {
@@ -90,6 +96,7 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
const int httpStatusCodeConflict = 409;
const int httpStatusCodeNotFound = 404;
const int httpStatusCodeNotImplemented = 501;
const int httpStatusCodeUnprocessableEntity = 422;
if (!sslErrors.empty()) {
qDebug().noquote() << sslErrors;
@@ -122,6 +129,8 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
return amnezia::ErrorCode::ApiNotFoundError;
} else if (httpStatusFromBody == httpStatusCodeNotImplemented) {
return amnezia::ErrorCode::ApiUpdateRequestError;
} else if (httpStatusFromBody == httpStatusCodeUnprocessableEntity) {
return amnezia::ErrorCode::ApiSubscriptionExpiredError;
}
return amnezia::ErrorCode::ApiConfigDownloadError;
}
@@ -133,7 +142,8 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
bool apiUtils::isPremiumServer(const QJsonObject &serverConfigObject)
{
static const QSet<apiDefs::ConfigType> premiumTypes = { apiDefs::ConfigType::AmneziaPremiumV1, apiDefs::ConfigType::AmneziaPremiumV2,
apiDefs::ConfigType::ExternalPremium };
apiDefs::ConfigType::AmneziaTrialV2, apiDefs::ConfigType::ExternalPremium,
apiDefs::ConfigType::ExternalTrial };
return premiumTypes.contains(getConfigType(serverConfigObject));
}
@@ -177,7 +187,9 @@ QString apiUtils::getPremiumV1VpnKey(const QJsonObject &serverConfigObject)
QString apiUtils::getPremiumV2VpnKey(const QJsonObject &serverConfigObject)
{
if (apiUtils::getConfigType(serverConfigObject) != apiDefs::ConfigType::AmneziaPremiumV2) {
auto configType = apiUtils::getConfigType(serverConfigObject);
if (configType != apiDefs::ConfigType::AmneziaPremiumV2 && configType != apiDefs::ConfigType::AmneziaTrialV2
&& configType != apiDefs::ConfigType::ExternalPremium && configType != apiDefs::ConfigType::ExternalTrial) {
return {};
}

View File

@@ -135,7 +135,7 @@ void CoreController::initControllers()
new SettingsController(m_serversModel, m_containersModel, m_languageModel, m_sitesModel, m_appSplitTunnelingModel, m_settings));
m_engine->rootContext()->setContextProperty("SettingsController", m_settingsController.get());
m_sitesController.reset(new SitesController(m_settings, m_vpnConnection, m_sitesModel));
m_sitesController.reset(new SitesController(m_settings, m_sitesModel));
m_engine->rootContext()->setContextProperty("SitesController", m_sitesController.get());
m_allowedDnsController.reset(new AllowedDnsController(m_settings, m_allowedDnsModel));
@@ -153,6 +153,8 @@ void CoreController::initControllers()
m_apiConfigsController.reset(new ApiConfigsController(m_serversModel, m_apiServicesModel, m_settings));
m_engine->rootContext()->setContextProperty("ApiConfigsController", m_apiConfigsController.get());
connect(m_apiConfigsController.get(), &ApiConfigsController::subscriptionRefreshNeeded,
this, [this]() { m_apiSettingsController->getAccountInfo(false); });
m_apiNewsController.reset(new ApiNewsController(m_newsModel, m_settings, m_serversModel, this));
m_engine->rootContext()->setContextProperty("ApiNewsController", m_apiNewsController.get());
@@ -368,7 +370,11 @@ void CoreController::initPrepareConfigHandler()
return;
}
if (!m_installController->isConfigValid()) {
m_installController->validateConfig();
});
connect(m_installController.get(), &InstallController::configValidated, this, [this](bool isValid) {
if (!isValid) {
emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected);
return;
}

View File

@@ -46,6 +46,7 @@ namespace
constexpr int httpStatusCodeConflict = 409;
constexpr int httpStatusCodeNotImplemented = 501;
constexpr int httpStatusCodeUnprocessableEntity = 422;
}
GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
@@ -337,6 +338,9 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
} else {
baseUrls = QString(PROD_S3_ENDPOINT).split(", ");
}
std::random_device randomDevice;
std::mt19937 generator(randomDevice());
std::shuffle(baseUrls.begin(), baseUrls.end(), generator);
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
@@ -448,6 +452,8 @@ bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &rep
}
} else if (httpStatus == httpStatusCodeConflict) {
return false;
} else if (httpStatus == httpStatusCodeUnprocessableEntity) {
return false;
} else if (replyError != QNetworkReply::NetworkError::NoError) {
qDebug() << replyError;
return true;

View File

@@ -72,9 +72,9 @@ void NetworkWatcher::initialize() {
connect(m_impl, &NetworkWatcherImpl::unsecuredNetwork, this,
&NetworkWatcher::unsecuredNetwork);
connect(m_impl, &NetworkWatcherImpl::networkChanged, this,
&NetworkWatcher::networkChange);
connect(m_impl, &NetworkWatcherImpl::sleepMode, this,
&NetworkWatcher::onSleepMode);
&NetworkWatcher::networkChanged);
connect(m_impl, &NetworkWatcherImpl::wakeup, this,
&NetworkWatcher::wakeup);
m_impl->initialize();
// Enable sleep/wake monitoring for VPN auto-reconnection
@@ -97,12 +97,6 @@ void NetworkWatcher::settingsChanged() {
logger.debug() << "NetworkWatcher settings changed - keeping sleep monitoring active";
}
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)

View File

@@ -29,13 +29,11 @@ public:
// false to restore.
void simulateDisconnection(bool simulatedDisconnection);
void onSleepMode();
QNetworkInformation::Reachability getReachability();
signals:
void networkChange();
void sleepMode();
void networkChanged();
void wakeup();
private:
void settingsChanged();

View File

@@ -41,7 +41,7 @@ signals:
// TODO: Only windows-networkwatcher has this, the other plattforms should
// too.
void networkChanged(QString newBSSID);
void sleepMode();
void wakeup();
private:

View File

@@ -101,7 +101,9 @@ bool AndroidController::initialize()
{"onAuthResult", "(Z)V", reinterpret_cast<void *>(onAuthResult)},
{"decodeQrCode", "(Ljava/lang/String;)Z", reinterpret_cast<bool *>(decodeQrCode)},
{"onImeInsetsChanged", "(I)V", reinterpret_cast<void *>(onImeInsetsChanged)},
{"onSystemBarsInsetsChanged", "(II)V", reinterpret_cast<void *>(onSystemBarsInsetsChanged)}
{"onSystemBarsInsetsChanged", "(II)V", reinterpret_cast<void *>(onSystemBarsInsetsChanged)},
{"onActivityPaused", "()V", reinterpret_cast<void *>(onActivityPaused)},
{"onActivityResumed", "()V", reinterpret_cast<void *>(onActivityResumed)}
};
QJniEnvironment env;
@@ -558,3 +560,22 @@ void AndroidController::onSystemBarsInsetsChanged(JNIEnv *env, jobject thiz, jin
emit AndroidController::instance()->systemBarsInsetsChanged(navBarHeightDp, statusBarHeightDp);
}
// static
void AndroidController::onActivityPaused(JNIEnv *env, jobject thiz)
{
Q_UNUSED(env);
Q_UNUSED(thiz);
emit AndroidController::instance()->activityPaused();
}
// static
void AndroidController::onActivityResumed(JNIEnv *env, jobject thiz)
{
Q_UNUSED(env);
Q_UNUSED(thiz);
emit AndroidController::instance()->activityResumed();
}

View File

@@ -75,6 +75,8 @@ signals:
void authenticationResult(bool result);
void imeInsetsChanged(int heightDp);
void systemBarsInsetsChanged(int navBarHeightDp, int statusBarHeightDp);
void activityPaused();
void activityResumed();
private:
bool isWaitingStatus = true;
@@ -105,6 +107,8 @@ private:
static bool decodeQrCode(JNIEnv *env, jobject thiz, jstring data);
static void onImeInsetsChanged(JNIEnv *env, jobject thiz, jint heightDp);
static void onSystemBarsInsetsChanged(JNIEnv *env, jobject thiz, jint navBarHeightDp, jint statusBarHeightDp);
static void onActivityPaused(JNIEnv *env, jobject thiz);
static void onActivityResumed(JNIEnv *env, jobject thiz);
template <typename Ret, typename ...Args>
static auto callActivityMethod(const char *methodName, const char *signature, Args &&...args);

View File

@@ -126,8 +126,7 @@ extension PacketTunnelProvider {
}
vpnReachability.startTracking { [weak self] status in
guard status == .reachableViaWiFi else { return }
self?.ovpnAdapter?.reconnect(afterTimeInterval: 5)
self?.handleOpenVPNReachabilityChange(status)
}
startHandler = completionHandler

View File

@@ -21,6 +21,44 @@ extension Constants {
}
extension PacketTunnelProvider {
private func applyXraySplitTunnel(_ xrayConfig: XrayConfig,
settings: NEPacketTunnelNetworkSettings) {
guard let splitTunnelType = xrayConfig.splitTunnelType else {
return
}
guard let splitTunnelSites = xrayConfig.splitTunnelSites else {
xrayLog(.error, message: "Split tunnel sites are not set")
return
}
if splitTunnelType == 1 {
var ipv4IncludedRoutes = [NEIPv4Route]()
for allowedIPString in splitTunnelSites {
if let allowedIP = IPAddressRange(from: allowedIPString) {
ipv4IncludedRoutes.append(NEIPv4Route(
destinationAddress: "\(allowedIP.address)",
subnetMask: "\(allowedIP.subnetMask())"))
}
}
settings.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
} else if splitTunnelType == 2 {
var ipv4ExcludedRoutes = [NEIPv4Route]()
for excludedIPString in splitTunnelSites {
if let excludedIP = IPAddressRange(from: excludedIPString) {
ipv4ExcludedRoutes.append(NEIPv4Route(
destinationAddress: "\(excludedIP.address)",
subnetMask: "\(excludedIP.subnetMask())"))
}
}
settings.ipv4Settings?.excludedRoutes = ipv4ExcludedRoutes
}
}
func startXray(completionHandler: @escaping (Error?) -> Void) {
// Xray configuration
@@ -72,6 +110,7 @@ extension PacketTunnelProvider {
settings.dnsSettings = !dnsArray.isEmpty
? NEDNSSettings(servers: dnsArray)
: NEDNSSettings(servers: ["1.1.1.1"])
applyXraySplitTunnel(xrayConfig, settings: settings)
let xrayConfigData = xrayConfig.config.data(using: .utf8)

View File

@@ -41,10 +41,15 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
var ovpnAdapter: OpenVPNAdapter?
private lazy var openVPNPacketFlowAdapter = PacketTunnelFlowAdapter(flow: packetFlow)
private let pathMonitorQueue = DispatchQueue(label: Constants.processQueueName + ".path-monitor")
private let networkChangeQueue = DispatchQueue(label: Constants.processQueueName + ".network-change")
private let pathMonitor = NWPathMonitor()
private var didReceiveInitialPathUpdate = false
private var currentPath: Network.NWPath?
private var currentPathSignature: String?
private var pendingOpenVPNReconnectWorkItem: DispatchWorkItem?
private var pendingNetworkChangeWorkItem: DispatchWorkItem?
private var isApplyingNetworkChange = false
private var lastOpenVPNReachabilityStatus: OpenVPNReachabilityStatus?
var splitTunnelType: Int?
var splitTunnelSites: [String]?
@@ -78,14 +83,22 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
guard hasMeaningfulChange, let proto = self.protoType else { return }
// WireGuard/AWG manages network changes internally; avoid restarting the tunnel here.
// WireGuard/AWG manages network changes internally in its own adapter.
if proto == .wireguard {
return
}
DispatchQueue.main.async {
self.handle(networkChange: path) { _ in }
if proto == .openvpn {
self.scheduleOpenVPNReconnect(reason: "NWPath changed")
return
}
if self.isApplyingNetworkChange || self.reasserting {
xrayLog(.debug, message: "Ignoring path change while xray restart is in progress")
return
}
self.scheduleNetworkChangeHandling(for: proto, path: path)
}
pathMonitor.start(queue: pathMonitorQueue)
@@ -197,6 +210,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
return
}
cancelPendingOpenVPNReconnect()
cancelPendingNetworkChangeHandling()
didReceiveInitialPathUpdate = false
updateActiveInterfaceIndexForCurrentPath()
@@ -215,6 +230,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
cancelPendingOpenVPNReconnect()
cancelPendingNetworkChangeHandling()
guard let protoType else {
completionHandler()
return
@@ -259,9 +277,111 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
}
private func handle(networkChange changePath: Network.NWPath, completion: @escaping (Error?) -> Void) {
guard protoType == .xray else {
updateActiveInterfaceIndex(for: changePath)
completion(nil)
return
}
updateActiveInterfaceIndex(for: changePath)
wg_log(.info, message: "Tunnel restarted.")
startTunnel(options: nil, completionHandler: completion)
reasserting = true
xrayLog(.info, message: "Applying network change to xray tunnel")
stopXray { }
startXray { [weak self] error in
self?.reasserting = false
completion(error)
}
}
private func scheduleNetworkChangeHandling(for proto: TunnelProtoType, path: Network.NWPath) {
guard proto == .xray else { return }
pendingNetworkChangeWorkItem?.cancel()
let workItem = DispatchWorkItem { [weak self] in
guard let self else { return }
self.pendingNetworkChangeWorkItem = nil
if self.isApplyingNetworkChange || self.reasserting {
xrayLog(.debug, message: "Skipping network change while restart is already in progress")
return
}
self.isApplyingNetworkChange = true
DispatchQueue.main.async {
self.handle(networkChange: path) { [weak self] _ in
self?.networkChangeQueue.async {
self?.isApplyingNetworkChange = false
}
}
}
}
pendingNetworkChangeWorkItem = workItem
networkChangeQueue.asyncAfter(deadline: .now() + 1.0, execute: workItem)
}
private func scheduleOpenVPNReconnect(reason: String) {
guard protoType == .openvpn else { return }
pendingOpenVPNReconnectWorkItem?.cancel()
let workItem = DispatchWorkItem { [weak self] in
guard let self else { return }
self.pendingOpenVPNReconnectWorkItem = nil
guard self.protoType == .openvpn else { return }
if self.reasserting {
ovpnLog(.debug, message: "Skipping OpenVPN reconnect while session is already reasserting")
return
}
DispatchQueue.main.async { [weak self] in
guard let self else { return }
guard !self.reasserting else {
ovpnLog(.debug, message: "Skipping OpenVPN reconnect while session is already reasserting")
return
}
ovpnLog(.info, message: "\(reason), reconnecting OpenVPN session")
self.ovpnAdapter?.reconnect(afterTimeInterval: 1)
}
}
pendingOpenVPNReconnectWorkItem = workItem
networkChangeQueue.asyncAfter(deadline: .now() + 1.0, execute: workItem)
}
func handleOpenVPNReachabilityChange(_ status: OpenVPNReachabilityStatus) {
defer { lastOpenVPNReachabilityStatus = status }
guard let previousStatus = lastOpenVPNReachabilityStatus else {
return
}
guard previousStatus != status else {
return
}
switch status {
case .reachableViaWiFi, .reachableViaWWAN:
scheduleOpenVPNReconnect(reason: "Reachability changed")
default:
break
}
}
private func cancelPendingOpenVPNReconnect() {
pendingOpenVPNReconnectWorkItem?.cancel()
pendingOpenVPNReconnectWorkItem = nil
lastOpenVPNReachabilityStatus = nil
}
private func cancelPendingNetworkChangeHandling() {
pendingNetworkChangeWorkItem?.cancel()
pendingNetworkChangeWorkItem = nil
isApplyingNetworkChange = false
}
}
@@ -271,8 +391,14 @@ private extension PacketTunnelProvider {
signatureComponents.append(path.isExpensive ? "exp" : "noexp")
signatureComponents.append(path.isConstrained ? "con" : "nocon")
let preferredTypes: [NWInterface.InterfaceType] = [.wiredEthernet, .wifi, .cellular, .loopback, .other]
let sortedInterfaces = path.availableInterfaces.sorted { lhs, rhs in
// Ignore loopback and tunnel-style `.other` interfaces so Xray does not
// react to its own utun lifecycle as if the physical uplink changed.
let preferredTypes: [NWInterface.InterfaceType] = [.wiredEthernet, .wifi, .cellular]
let externalInterfaces = path.availableInterfaces.filter { interface in
interface.type == .wiredEthernet || interface.type == .wifi || interface.type == .cellular
}
let sortedInterfaces = externalInterfaces.sorted { lhs, rhs in
if lhs.type == rhs.type {
return lhs.index < rhs.index
}
@@ -293,8 +419,8 @@ private extension PacketTunnelProvider {
case .wiredEthernet: typeName = "ethernet"
case .wifi: typeName = "wifi"
case .cellular: typeName = "cellular"
case .loopback: typeName = "loopback"
case .other: typeName = "other"
case .loopback, .other:
continue
@unknown default: typeName = "unknown"
}
signatureComponents.append("\(typeName):\(interface.index)")

View File

@@ -3,5 +3,7 @@ import Foundation
struct XrayConfig: Decodable {
let dns1: String?
let dns2: String?
let splitTunnelType: Int?
let splitTunnelSites: [String]?
let config: String
}

View File

@@ -684,6 +684,15 @@ bool IosController::setupXray()
QJsonObject finalConfig;
finalConfig.insert(config_key::dns1, m_rawConfig[config_key::dns1].toString());
finalConfig.insert(config_key::dns2, m_rawConfig[config_key::dns2].toString());
finalConfig.insert(config_key::splitTunnelType, m_rawConfig[config_key::splitTunnelType]);
QJsonArray splitTunnelSites = m_rawConfig[config_key::splitTunnelSites].toArray();
for(int index = 0; index < splitTunnelSites.count(); index++) {
splitTunnelSites[index] = splitTunnelSites[index].toString().remove(" ");
}
finalConfig.insert(config_key::splitTunnelSites, splitTunnelSites);
finalConfig.insert(config_key::config, xrayConfigStr);
QJsonDocument finalConfigDoc(finalConfig);

View File

@@ -108,7 +108,7 @@ int LinuxFirewall::linkChain(LinuxFirewall::IPVersion ip, const QString& chain,
// (we can't safely delete all rules at once since rule numbers change)
// TODO: occasionally this script results in warnings in logs "Bad rule (does a matching rule exist in the chain?)" - this happens when
// the e.g OUTPUT chain is empty but this script attempts to delete things from it anyway. It doesn't cause any problems, but we should still fix at some point..
return execute(QStringLiteral("if ! %1 -L %2 -n --line-numbers -t %4 2> /dev/null | awk 'int($1) == 1 && $2 == \"%3\" { found=1 } END { if(found==1) { exit 0 } else { exit 1 } }' ; then %1 -I %2 -j %3 -t %4 && %1 -L %2 -n --line-numbers -t %4 2> /dev/null | awk 'int($1) > 1 && $2 == \"%3\" { print $1; exit }' | xargs %1 -t %4 -D %2 ; fi").arg(cmd, parent, chain, tableName));
return execute(QStringLiteral("if ! %1 -L %2 -n --line-numbers -t %4 2> /dev/null | awk 'int($1) == 1 && $2 == \"%3\" { found=1 } END { if(found==1) { exit 0 } else { exit 1 } }' ; then %1 -I %2 -j %3 -t %4 && %1 -L %2 -n --line-numbers -t %4 2> /dev/null | awk 'int($1) > 1 && $2 == \"%3\" { print $1; exit }' | xargs -r %1 -t %4 -D %2 ; fi").arg(cmd, parent, chain, tableName));
}
else
return execute(QStringLiteral("if ! %1 -C %2 -j %3 -t %4 2> /dev/null ; then %1 -A %2 -j %3 -t %4; fi").arg(cmd, parent, chain, tableName));
@@ -289,6 +289,7 @@ void LinuxFirewall::install()
installAnchor(Both, QStringLiteral("100.blockAll"), {
QStringLiteral("-j REJECT"),
});
installAnchor(Both, QStringLiteral("400.allowPIA"), {});
// NAT rules
installAnchor(Both, QStringLiteral("100.transIp"), {
@@ -495,13 +496,23 @@ int LinuxFirewall::execute(const QString &command, bool ignoreErrors)
logger.debug() << "(" << exitCode << ") $ " << command;
if (!out.isEmpty())
logger.info() << out;
if (!err.isEmpty())
if (!err.isEmpty() && !ignoreErrors)
logger.warning() << err;
return exitCode;
}
void LinuxFirewall::setupTrafficSplitting()
{
// Register the routing table name so the ip tool can resolve it by name
execute(QStringLiteral("grep -q '%1' /etc/iproute2/rt_tables || printf '200\\t%1\\n' >> /etc/iproute2/rt_tables").arg(kRtableName));
// On cgroup v2 (unified hierarchy) systems /sys/fs/cgroup/net_cls/ does not exist.
// Try to mount the legacy net_cls cgroup v1 controller so traffic splitting works.
execute(QStringLiteral(
"if [ ! -d /sys/fs/cgroup/net_cls ] ; then "
"mkdir -p /sys/fs/cgroup/net_cls && "
"mount -t cgroup -o net_cls cgroup /sys/fs/cgroup/net_cls ; fi"));
auto cGroupDir = "/sys/fs/cgroup/net_cls/" BRAND_CODE "vpnexclusions/";
logger.info() << "Should be setting up cgroup in" << cGroupDir << "for traffic splitting";
execute(QStringLiteral("if [ ! -d %1 ] ; then mkdir %1 ; sleep 0.1 ; echo %2 > %1/net_cls.classid ; fi").arg(cGroupDir).arg(kCGroupId));
@@ -513,6 +524,9 @@ void LinuxFirewall::teardownTrafficSplitting()
{
logger.info() << "Tearing down cgroup and routing rules";
execute(QStringLiteral("if ip rule list | grep -q %1; then ip rule del from all fwmark %1 lookup %2 2> /dev/null ; fi").arg(kPacketTag, kRtableName));
execute(QStringLiteral("ip route flush table %1").arg(kRtableName));
// Ignore errors here: on first teardown the table name may not be registered yet
execute(QStringLiteral("ip route flush table %1").arg(kRtableName), true);
execute(QStringLiteral("ip route flush cache"));
// Remove the rt_tables entry we added
execute(QStringLiteral("sed -i '/%1/d' /etc/iproute2/rt_tables").arg(kRtableName));
}

View File

@@ -41,8 +41,8 @@ void LinuxNetworkWatcher::initialize() {
connect(m_worker, &LinuxNetworkWatcherWorker::unsecuredNetwork, this,
&LinuxNetworkWatcher::unsecuredNetwork);
connect(m_worker, &LinuxNetworkWatcherWorker::sleepMode, this,
&NetworkWatcherImpl::sleepMode);
connect(m_worker, &LinuxNetworkWatcherWorker::wakeup, this,
&NetworkWatcherImpl::wakeup);
// 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

View File

@@ -200,7 +200,7 @@ void LinuxNetworkWatcherWorker::checkDevices() {
void LinuxNetworkWatcherWorker::NMStateChanged(quint32 state)
{
if (state == NM_STATE_ASLEEP) {
emit sleepMode();
emit wakeup();
}
logger.debug() << "NMStateChanged " << state;

View File

@@ -23,7 +23,7 @@ class LinuxNetworkWatcherWorker final : public QObject {
signals:
void unsecuredNetwork(const QString& networkName, const QString& networkId);
void sleepMode();
void wakeup();
public slots:
void initialize();

View File

@@ -173,10 +173,10 @@ void PowerNotificationsListener::sleepWakeupCallBack(void *refParam, io_service_
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";
logger.debug() << "System has powered on - emitting wakeup 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);
QMetaObject::invokeMethod(listener->m_watcher, "wakeup", Qt::QueuedConnection);
}
break;

View File

@@ -41,7 +41,7 @@ LRESULT WindowsNetworkWatcher::PowerWndProcCallback(HWND hwnd, UINT uMsg, WPARAM
switch (uMsg) {
case WM_POWERBROADCAST:
if (wParam == PBT_APMRESUMESUSPEND) {
emit obj->sleepMode();
emit obj->wakeup();
}
break;
default:

View File

@@ -15,7 +15,7 @@ WireguardProtocol::WireguardProtocol(const QJsonObject &configuration, QObject *
m_impl.reset(new LocalSocketController());
connect(m_impl.get(), &ControllerImpl::connected, this,
[this](const QString &pubkey, const QDateTime &connectionTimestamp) {
emit connectionStateChanged(Vpn::ConnectionState::Connected);
setConnectionState(Vpn::ConnectionState::Connected);
});
connect(m_impl.get(), &ControllerImpl::statusUpdated, this,
[this](const QString& serverIpv4Gateway,
@@ -38,7 +38,7 @@ WireguardProtocol::WireguardProtocol(const QJsonObject &configuration, QObject *
});
connect(m_impl.get(), &ControllerImpl::disconnected, this,
[this]() { emit connectionStateChanged(Vpn::ConnectionState::Disconnected); });
[this]() { setConnectionState(Vpn::ConnectionState::Disconnected); });
m_impl->initialize(nullptr, nullptr);
}

View File

@@ -52,7 +52,6 @@ XrayProtocol::~XrayProtocol()
ErrorCode XrayProtocol::start()
{
qDebug() << "XrayProtocol::start()";
setConnectionState(Vpn::ConnectionState::Connecting);
return IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
auto xrayStart = iface->xrayStart(QJsonDocument(m_xrayConfig).toJson());
@@ -69,7 +68,6 @@ ErrorCode XrayProtocol::start()
void XrayProtocol::stop()
{
qDebug() << "XrayProtocol::stop()";
setConnectionState(Vpn::ConnectionState::Disconnecting);
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
auto disableKillSwitch = iface->disableKillSwitch();

View File

@@ -135,6 +135,7 @@
<file>ui/qml/Components/InstalledAppsDrawer.qml</file>
<file>ui/qml/Components/QuestionDrawer.qml</file>
<file>ui/qml/Components/SelectLanguageDrawer.qml</file>
<file>ui/qml/Components/SubscriptionExpiredDrawer.qml</file>
<file>ui/qml/Components/ServersListView.qml</file>
<file>ui/qml/Components/SettingsContainersListView.qml</file>
<file>ui/qml/Components/TransportProtoSelector.qml</file>

View File

@@ -35,13 +35,12 @@ SecureQSettings::SecureQSettings(const QString &organization, const QString &app
}
}
m_settings.setValue("Conf/encrypted", true);
m_settings.sync();
}
}
QVariant SecureQSettings::value(const QString &key, const QVariant &defaultValue) const
{
QMutexLocker locker(&mutex);
QMutexLocker locker(&m_mutex);
if (m_cache.contains(key)) {
return m_cache.value(key);
@@ -85,7 +84,7 @@ QVariant SecureQSettings::value(const QString &key, const QVariant &defaultValue
void SecureQSettings::setValue(const QString &key, const QVariant &value)
{
QMutexLocker locker(&mutex);
QMutexLocker locker(&m_mutex);
if (encryptionRequired() && encryptedKeys.contains(key)) {
if (!getEncKey().isEmpty() && !getEncIv().isEmpty()) {
@@ -107,26 +106,20 @@ void SecureQSettings::setValue(const QString &key, const QVariant &value)
}
m_cache.insert(key, value);
sync();
}
void SecureQSettings::remove(const QString &key)
{
QMutexLocker locker(&mutex);
QMutexLocker locker(&m_mutex);
m_settings.remove(key);
m_cache.remove(key);
sync();
}
void SecureQSettings::sync()
{
m_settings.sync();
}
QByteArray SecureQSettings::backupAppConfig() const
{
QMutexLocker locker(&m_mutex);
QJsonObject cfg;
const auto needToBackup = [this](const auto &key) {
@@ -161,6 +154,8 @@ QByteArray SecureQSettings::backupAppConfig() const
bool SecureQSettings::restoreAppConfig(const QByteArray &json)
{
QMutexLocker locker(&m_mutex);
QJsonObject cfg = QJsonDocument::fromJson(json).object();
if (cfg.isEmpty())
return false;
@@ -173,10 +168,16 @@ bool SecureQSettings::restoreAppConfig(const QByteArray &json)
setValue(key, cfg.value(key).toVariant());
}
sync();
return true;
}
void SecureQSettings::clearSettings()
{
QMutexLocker locker(&m_mutex);
m_settings.clear();
m_cache.clear();
}
QByteArray SecureQSettings::encryptText(const QByteArray &value) const
{
QSimpleCrypto::QBlockCipher cipher;
@@ -294,11 +295,3 @@ void SecureQSettings::setSecTag(const QString &tag, const QByteArray &data)
qCritical() << "SecureQSettings::setSecTag Error:" << job->errorString();
}
}
void SecureQSettings::clearSettings()
{
QMutexLocker locker(&mutex);
m_settings.clear();
m_cache.clear();
sync();
}

View File

@@ -16,14 +16,16 @@ public:
explicit SecureQSettings(const QString &organization, const QString &application = QString(),
QObject *parent = nullptr);
Q_INVOKABLE QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const;
Q_INVOKABLE void setValue(const QString &key, const QVariant &value);
QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const;
void setValue(const QString &key, const QVariant &value);
void remove(const QString &key);
void sync();
QByteArray backupAppConfig() const;
bool restoreAppConfig(const QByteArray &json);
void clearSettings();
private:
QByteArray encryptText(const QByteArray &value) const;
QByteArray decryptText(const QByteArray &ba) const;
@@ -35,9 +37,6 @@ public:
static QByteArray getSecTag(const QString &tag);
static void setSecTag(const QString &tag, const QByteArray &data);
void clearSettings();
private:
QSettings m_settings;
mutable QHash<QString, QVariant> m_cache;
@@ -53,7 +52,7 @@ private:
const QByteArray magicString { "EncData" }; // Magic keyword used for mark encrypted QByteArray
mutable QMutex mutex;
mutable QRecursiveMutex m_mutex;
};
#endif // SECUREQSETTINGS_H

View File

@@ -21,10 +21,10 @@ Settings::Settings(QObject *parent) : QObject(parent), m_settings(ORGANIZATION_N
{
// Import old settings
if (serversCount() == 0) {
QString user = value("Server/userName").toString();
QString password = value("Server/password").toString();
QString serverName = value("Server/serverName").toString();
int port = value("Server/serverPort").toInt();
QString user = m_settings.value("Server/userName").toString();
QString password = m_settings.value("Server/password").toString();
QString serverName = m_settings.value("Server/serverName").toString();
int port = m_settings.value("Server/serverPort").toInt();
if (!user.isEmpty() && !password.isEmpty() && !serverName.isEmpty()) {
QJsonObject server;
@@ -222,7 +222,7 @@ QString Settings::nextAvailableServerName() const
void Settings::setSaveLogs(bool enabled)
{
setValue("Conf/saveLogs", enabled);
m_settings.setValue("Conf/saveLogs", enabled);
#ifndef Q_OS_ANDROID
if (!isSaveLogs()) {
Logger::deInit();
@@ -242,12 +242,12 @@ void Settings::setSaveLogs(bool enabled)
QDateTime Settings::getLogEnableDate()
{
return value("Conf/logEnableDate").toDateTime();
return m_settings.value("Conf/logEnableDate").toDateTime();
}
void Settings::setLogEnableDate(QDateTime date)
{
setValue("Conf/logEnableDate", date);
m_settings.setValue("Conf/logEnableDate", date);
}
QString Settings::routeModeString(RouteMode mode) const
@@ -261,17 +261,17 @@ QString Settings::routeModeString(RouteMode mode) const
Settings::RouteMode Settings::routeMode() const
{
return static_cast<RouteMode>(value("Conf/routeMode", 0).toInt());
return static_cast<RouteMode>(m_settings.value("Conf/routeMode", 0).toInt());
}
bool Settings::isSitesSplitTunnelingEnabled() const
{
return value("Conf/sitesSplitTunnelingEnabled", false).toBool();
return m_settings.value("Conf/sitesSplitTunnelingEnabled", false).toBool();
}
void Settings::setSitesSplitTunnelingEnabled(bool enabled)
{
setValue("Conf/sitesSplitTunnelingEnabled", enabled);
m_settings.setValue("Conf/sitesSplitTunnelingEnabled", enabled);
}
bool Settings::addVpnSite(RouteMode mode, const QString &site, const QString &ip)
@@ -359,12 +359,12 @@ void Settings::removeAllVpnSites(RouteMode mode)
QString Settings::primaryDns() const
{
return value("Conf/primaryDns", cloudFlareNs1).toString();
return m_settings.value("Conf/primaryDns", cloudFlareNs1).toString();
}
QString Settings::secondaryDns() const
{
return value("Conf/secondaryDns", cloudFlareNs2).toString();
return m_settings.value("Conf/secondaryDns", cloudFlareNs2).toString();
}
void Settings::clearSettings()
@@ -386,18 +386,18 @@ QString Settings::appsRouteModeString(AppsRouteMode mode) const
Settings::AppsRouteMode Settings::getAppsRouteMode() const
{
return static_cast<AppsRouteMode>(value("Conf/appsRouteMode", 0).toInt());
return static_cast<AppsRouteMode>(m_settings.value("Conf/appsRouteMode", 0).toInt());
}
void Settings::setAppsRouteMode(AppsRouteMode mode)
{
setValue("Conf/appsRouteMode", mode);
m_settings.setValue("Conf/appsRouteMode", mode);
}
QVector<InstalledAppInfo> Settings::getVpnApps(AppsRouteMode mode) const
{
QVector<InstalledAppInfo> apps;
auto appsArray = value("Conf/" + appsRouteModeString(mode)).toJsonArray();
auto appsArray = m_settings.value("Conf/" + appsRouteModeString(mode)).toJsonArray();
for (const auto &app : appsArray) {
InstalledAppInfo appInfo;
appInfo.appName = app.toObject().value("appName").toString();
@@ -419,43 +419,42 @@ void Settings::setVpnApps(AppsRouteMode mode, const QVector<InstalledAppInfo> &a
appInfo.insert("appPath", app.appPath);
appsArray.push_back(appInfo);
}
setValue("Conf/" + appsRouteModeString(mode), appsArray);
m_settings.sync();
m_settings.setValue("Conf/" + appsRouteModeString(mode), appsArray);
}
bool Settings::isAppsSplitTunnelingEnabled() const
{
return value("Conf/appsSplitTunnelingEnabled", false).toBool();
return m_settings.value("Conf/appsSplitTunnelingEnabled", false).toBool();
}
void Settings::setAppsSplitTunnelingEnabled(bool enabled)
{
setValue("Conf/appsSplitTunnelingEnabled", enabled);
m_settings.setValue("Conf/appsSplitTunnelingEnabled", enabled);
}
bool Settings::isKillSwitchEnabled() const
{
return value("Conf/killSwitchEnabled", true).toBool();
return m_settings.value("Conf/killSwitchEnabled", true).toBool();
}
void Settings::setKillSwitchEnabled(bool enabled)
{
setValue("Conf/killSwitchEnabled", enabled);
m_settings.setValue("Conf/killSwitchEnabled", enabled);
}
bool Settings::isStrictKillSwitchEnabled() const
{
return value("Conf/strictKillSwitchEnabled", false).toBool();
return m_settings.value("Conf/strictKillSwitchEnabled", false).toBool();
}
void Settings::setStrictKillSwitchEnabled(bool enabled)
{
setValue("Conf/strictKillSwitchEnabled", enabled);
m_settings.setValue("Conf/strictKillSwitchEnabled", enabled);
}
QString Settings::getInstallationUuid(const bool needCreate)
{
auto uuid = value("Conf/installationUuid", "").toString();
auto uuid = m_settings.value("Conf/installationUuid", "").toString();
if (needCreate && uuid.isEmpty()) {
uuid = QUuid::createUuid().toString();
@@ -476,7 +475,7 @@ QString Settings::getInstallationUuid(const bool needCreate)
void Settings::setInstallationUuid(const QString &uuid)
{
setValue("Conf/installationUuid", uuid);
m_settings.setValue("Conf/installationUuid", uuid);
}
ServerCredentials Settings::defaultServerCredentials() const
@@ -497,28 +496,6 @@ ServerCredentials Settings::serverCredentials(int index) const
return credentials;
}
QVariant Settings::value(const QString &key, const QVariant &defaultValue) const
{
QVariant returnValue;
if (QThread::currentThread() == QCoreApplication::instance()->thread()) {
returnValue = m_settings.value(key, defaultValue);
} else {
QMetaObject::invokeMethod(&m_settings, "value", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QVariant, returnValue),
Q_ARG(const QString &, key), Q_ARG(const QVariant &, defaultValue));
}
return returnValue;
}
void Settings::setValue(const QString &key, const QVariant &value)
{
if (QThread::currentThread() == QCoreApplication::instance()->thread()) {
m_settings.setValue(key, value);
} else {
QMetaObject::invokeMethod(&m_settings, "setValue", Qt::BlockingQueuedConnection, Q_ARG(const QString &, key),
Q_ARG(const QVariant &, value));
}
}
void Settings::resetGatewayEndpoint()
{
m_gatewayEndpoint = gatewayEndpoint;
@@ -541,50 +518,50 @@ QString Settings::getGatewayEndpoint(bool isTestPurchase)
bool Settings::isDevGatewayEnv(bool isTestPurchase)
{
return isTestPurchase ? true : value("Conf/devGatewayEnv", false).toBool();
return isTestPurchase ? true : m_settings.value("Conf/devGatewayEnv", false).toBool();
}
void Settings::toggleDevGatewayEnv(bool enabled)
{
setValue("Conf/devGatewayEnv", enabled);
m_settings.setValue("Conf/devGatewayEnv", enabled);
}
bool Settings::isHomeAdLabelVisible()
{
return value("Conf/homeAdLabelVisible", true).toBool();
return m_settings.value("Conf/homeAdLabelVisible", true).toBool();
}
void Settings::disableHomeAdLabel()
{
setValue("Conf/homeAdLabelVisible", false);
m_settings.setValue("Conf/homeAdLabelVisible", false);
}
bool Settings::isPremV1MigrationReminderActive()
{
return value("Conf/premV1MigrationReminderActive", true).toBool();
return m_settings.value("Conf/premV1MigrationReminderActive", true).toBool();
}
void Settings::disablePremV1MigrationReminder()
{
setValue("Conf/premV1MigrationReminderActive", false);
m_settings.setValue("Conf/premV1MigrationReminderActive", false);
}
QStringList Settings::allowedDnsServers() const
{
return value("Conf/allowedDnsServers").toStringList();
return m_settings.value("Conf/allowedDnsServers").toStringList();
}
void Settings::setAllowedDnsServers(const QStringList &servers)
{
setValue("Conf/allowedDnsServers", servers);
m_settings.setValue("Conf/allowedDnsServers", servers);
}
QStringList Settings::readNewsIds() const
{
return value("News/readIds").toStringList();
return m_settings.value("News/readIds").toStringList();
}
void Settings::setReadNewsIds(const QStringList &ids)
{
setValue("News/readIds", ids);
m_settings.setValue("News/readIds", ids);
}

View File

@@ -29,11 +29,11 @@ public:
QJsonArray serversArray() const
{
return QJsonDocument::fromJson(value("Servers/serversList").toByteArray()).array();
return QJsonDocument::fromJson(m_settings.value("Servers/serversList").toByteArray()).array();
}
void setServersArray(const QJsonArray &servers)
{
setValue("Servers/serversList", QJsonDocument(servers).toJson());
m_settings.setValue("Servers/serversList", QJsonDocument(servers).toJson());
}
// Servers section
@@ -45,11 +45,11 @@ public:
int defaultServerIndex() const
{
return value("Servers/defaultServerIndex", 0).toInt();
return m_settings.value("Servers/defaultServerIndex", 0).toInt();
}
void setDefaultServer(int index)
{
setValue("Servers/defaultServerIndex", index);
m_settings.setValue("Servers/defaultServerIndex", index);
}
QJsonObject defaultServer() const
{
@@ -78,34 +78,34 @@ public:
// App settings section
bool isAutoConnect() const
{
return value("Conf/autoConnect", false).toBool();
return m_settings.value("Conf/autoConnect", false).toBool();
}
void setAutoConnect(bool enabled)
{
setValue("Conf/autoConnect", enabled);
m_settings.setValue("Conf/autoConnect", enabled);
}
bool isStartMinimized() const
{
return value("Conf/startMinimized", false).toBool();
return m_settings.value("Conf/startMinimized", false).toBool();
}
void setStartMinimized(bool enabled)
{
setValue("Conf/startMinimized", enabled);
m_settings.setValue("Conf/startMinimized", enabled);
}
bool isNewsNotifications() const
{
return value("Conf/newsNotifications", true).toBool();
return m_settings.value("Conf/newsNotifications", true).toBool();
}
void setNewsNotifications(bool enabled)
{
setValue("Conf/newsNotifications", enabled);
m_settings.setValue("Conf/newsNotifications", enabled);
}
bool isSaveLogs() const
{
return value("Conf/saveLogs", false).toBool();
return m_settings.value("Conf/saveLogs", false).toBool();
}
void setSaveLogs(bool enabled);
@@ -122,19 +122,18 @@ public:
QString routeModeString(RouteMode mode) const;
RouteMode routeMode() const;
void setRouteMode(RouteMode mode) { setValue("Conf/routeMode", mode); }
void setRouteMode(RouteMode mode) { m_settings.setValue("Conf/routeMode", mode); }
bool isSitesSplitTunnelingEnabled() const;
void setSitesSplitTunnelingEnabled(bool enabled);
QVariantMap vpnSites(RouteMode mode) const
{
return value("Conf/" + routeModeString(mode)).toMap();
return m_settings.value("Conf/" + routeModeString(mode)).toMap();
}
void setVpnSites(RouteMode mode, const QVariantMap &sites)
{
setValue("Conf/" + routeModeString(mode), sites);
m_settings.sync();
m_settings.setValue("Conf/" + routeModeString(mode), sites);
}
bool addVpnSite(RouteMode mode, const QString &site, const QString &ip = "");
void addVpnSites(RouteMode mode, const QMap<QString, QString> &sites); // map <site, ip>
@@ -147,11 +146,11 @@ public:
bool useAmneziaDns() const
{
return value("Conf/useAmneziaDns", true).toBool();
return m_settings.value("Conf/useAmneziaDns", true).toBool();
}
void setUseAmneziaDns(bool enabled)
{
setValue("Conf/useAmneziaDns", enabled);
m_settings.setValue("Conf/useAmneziaDns", enabled);
}
QString primaryDns() const;
@@ -160,13 +159,13 @@ public:
// QString primaryDns() const { return m_primaryDns; }
void setPrimaryDns(const QString &primaryDns)
{
setValue("Conf/primaryDns", primaryDns);
m_settings.setValue("Conf/primaryDns", primaryDns);
}
// QString secondaryDns() const { return m_secondaryDns; }
void setSecondaryDns(const QString &secondaryDns)
{
setValue("Conf/secondaryDns", secondaryDns);
m_settings.setValue("Conf/secondaryDns", secondaryDns);
}
// static constexpr char openNicNs5[] = "94.103.153.176";
@@ -188,16 +187,16 @@ public:
};
void setAppLanguage(QLocale locale)
{
setValue("Conf/appLanguage", locale.name());
m_settings.setValue("Conf/appLanguage", locale.name());
};
bool isScreenshotsEnabled() const
{
return value("Conf/screenshotsEnabled", true).toBool();
return m_settings.value("Conf/screenshotsEnabled", true).toBool();
}
void setScreenshotsEnabled(bool enabled)
{
setValue("Conf/screenshotsEnabled", enabled);
m_settings.setValue("Conf/screenshotsEnabled", enabled);
emit screenshotsEnabledChanged(enabled);
}
@@ -255,9 +254,6 @@ signals:
void settingsCleared();
private:
QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const;
void setValue(const QString &key, const QVariant &value);
void setInstallationUuid(const QString &uuid);
mutable SecureQSettings m_settings;

File diff suppressed because it is too large Load Diff

View File

@@ -366,6 +366,8 @@ bool ApiConfigsController::fillAvailableServices()
{
QJsonObject apiPayload;
apiPayload[configKey::osVersion] = QSysInfo::productType();
apiPayload[configKey::appVersion] = QString(APP_VERSION);
apiPayload[apiDefs::key::cliName] = QString(APPLICATION_NAME);
apiPayload[apiDefs::key::appLanguage] = m_settings->getAppLanguage().name().split("_").first();
QByteArray responseBody;
@@ -447,7 +449,7 @@ bool ApiConfigsController::importService()
importSerivceFromAppStore();
return true;
}
} else {
} else if (m_apiServicesModel->getSelectedServiceType() == serviceType::amneziaFree) {
importServiceFromGateway();
return true;
}
@@ -721,6 +723,7 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
}
bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false);
bool wasSubscriptionExpired = m_serversModel->data(serverIndex, ServersModel::IsSubscriptionExpiredRole).toBool();
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody, isTestPurchase);
@@ -747,6 +750,11 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
newServerConfig.insert(config_key::nameOverriddenByUser, true);
}
m_serversModel->editServer(newServerConfig, serverIndex);
if (wasSubscriptionExpired) {
emit subscriptionRefreshNeeded();
}
if (reloadServiceConfig) {
emit reloadServerFromApiFinished(tr("API config reloaded"));
} else if (newCountryName.isEmpty()) {
@@ -756,7 +764,11 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
}
return true;
} else {
emit errorOccurred(errorCode);
if (errorCode == ErrorCode::ApiSubscriptionExpiredError) {
emit subscriptionExpiredOnServer();
} else {
emit errorOccurred(errorCode);
}
return false;
}
}

View File

@@ -43,6 +43,8 @@ public slots:
signals:
void errorOccurred(ErrorCode errorCode);
void subscriptionExpiredOnServer();
void subscriptionRefreshNeeded();
void installServerFromApiFinished(const QString &message);
void changeApiCountryFinished(const QString &message);

View File

@@ -1,6 +1,7 @@
#include "apiSettingsController.h"
#include <QEventLoop>
#include <QJsonDocument>
#include <QTimer>
#include "core/api/apiUtils.h"
@@ -77,6 +78,13 @@ bool ApiSettingsController::getAccountInfo(bool reload)
QJsonObject accountInfo = QJsonDocument::fromJson(responseBody).object();
m_apiAccountInfoModel->updateModel(accountInfo, serverConfig);
QString subscriptionEndDate = accountInfo.value(apiDefs::key::subscriptionEndDate).toString();
if (!subscriptionEndDate.isEmpty()) {
apiConfig.insert(apiDefs::key::subscriptionEndDate, subscriptionEndDate);
serverConfig.insert(configKey::apiConfig, apiConfig);
m_serversModel->editServer(serverConfig, processedIndex);
}
if (reload) {
updateApiCountryModel();
updateApiDevicesModel();
@@ -85,6 +93,42 @@ bool ApiSettingsController::getAccountInfo(bool reload)
return true;
}
void ApiSettingsController::getRenewalLink()
{
auto processedIndex = m_serversModel->getProcessedServerIndex();
auto serverConfig = m_serversModel->getServerConfig(processedIndex);
auto apiConfig = serverConfig.value(configKey::apiConfig).toObject();
auto authData = serverConfig.value(configKey::authData).toObject();
bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false);
auto gatewayController = QSharedPointer<GatewayController>::create(m_settings->getGatewayEndpoint(isTestPurchase),
m_settings->isDevGatewayEnv(isTestPurchase),
requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
QJsonObject apiPayload;
apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString();
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();
auto future = gatewayController->postAsync(QString("%1v1/renewal_link"), apiPayload);
future.then(this, [this, gatewayController](QPair<ErrorCode, QByteArray> result) {
auto [errorCode, responseBody] = result;
if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode);
return;
}
QJsonObject responseJson = QJsonDocument::fromJson(responseBody).object();
QString url = responseJson.value("renewal_url").toString();
if (!url.isEmpty()) {
emit renewalLinkReceived(url);
}
});
}
void ApiSettingsController::updateApiCountryModel()
{
m_apiCountryModel->updateModel(m_apiAccountInfoModel->getAvailableCountries(), "");

View File

@@ -21,9 +21,11 @@ public slots:
bool getAccountInfo(bool reload);
void updateApiCountryModel();
void updateApiDevicesModel();
void getRenewalLink();
signals:
void errorOccurred(ErrorCode errorCode);
void renewalLinkReceived(const QString &url);
private:
QSharedPointer<ServersModel> m_serversModel;

View File

@@ -7,7 +7,6 @@
#include <QFileInfo>
#include <QImage>
#include <QStandardPaths>
#include "core/controllers/vpnConfigurationController.h"
#include "core/qrCodeUtils.h"
#include "core/serialization/serialization.h"
@@ -170,8 +169,7 @@ void ExportController::generateWireGuardConfig(const QString &clientName)
m_config.append(line + "\n");
}
auto qr = qrCodeUtils::generateQrCode(m_config.toUtf8());
m_qrCodes << qrCodeUtils::svgToBase64(QString::fromStdString(toSvgString(qr, 1)));
m_qrCodes = qrCodeUtils::generateQrCodeImageSeries(m_config.toUtf8());
emit exportConfigChanged();
}
@@ -191,8 +189,7 @@ void ExportController::generateAwgConfig(const QString &clientName)
m_config.append(line + "\n");
}
auto qr = qrCodeUtils::generateQrCode(m_config.toUtf8());
m_qrCodes << qrCodeUtils::svgToBase64(QString::fromStdString(toSvgString(qr, 1)));
m_qrCodes = qrCodeUtils::generateQrCodeImageSeries(m_config.toUtf8());
emit exportConfigChanged();
}

View File

@@ -83,8 +83,8 @@ void InstallController::install(DockerContainer container, int port, TransportPr
int s1 = QRandomGenerator::global()->bounded(15, 150);
int s2 = QRandomGenerator::global()->bounded(15, 150);
int s3 = QRandomGenerator::global()->bounded(0, 64);
int s4 = QRandomGenerator::global()->bounded(0, 20);
int s3 = QRandomGenerator::global()->bounded(1, 64);
int s4 = QRandomGenerator::global()->bounded(1, 20);
// Ensure all values are unique and don't create equal packet sizes
QSet<int> usedValues;
@@ -97,12 +97,12 @@ void InstallController::install(DockerContainer container, int port, TransportPr
while (usedValues.contains(s3) || s1 + AwgConstant::messageInitiationSize == s3 + AwgConstant::messageCookieReplySize
|| s2 + AwgConstant::messageResponseSize == s3 + AwgConstant::messageCookieReplySize) {
s3 = QRandomGenerator::global()->bounded(0, 64);
s3 = QRandomGenerator::global()->bounded(1, 64);
}
usedValues.insert(s3);
while (usedValues.contains(s4)) {
s4 = QRandomGenerator::global()->bounded(0, 20);
s4 = QRandomGenerator::global()->bounded(1, 20);
}
QString initPacketJunkSize = QString::number(s1);
@@ -987,79 +987,94 @@ void InstallController::addEmptyServer()
emit installServerFinished(tr("Server added successfully"));
}
bool InstallController::isConfigValid()
void InstallController::validateConfig()
{
int serverIndex = m_serversModel->getDefaultServerIndex();
QJsonObject serverConfigObject = m_serversModel->getServerConfig(serverIndex);
if (apiUtils::isServerFromApi(serverConfigObject)) {
return true;
emit configValidated(true);
return;
}
if (!m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) {
emit noInstalledContainers();
return false;
emit configValidated(false);
return;
}
DockerContainer container = qvariant_cast<DockerContainer>(m_serversModel->data(serverIndex, ServersModel::Roles::DefaultContainerRole));
if (container == DockerContainer::None) {
emit installationErrorOccurred(ErrorCode::NoInstalledContainersError);
return false;
emit configValidated(false);
return;
}
QSharedPointer<ServerController> serverController(new ServerController(m_settings));
VpnConfigurationsController vpnConfigurationController(m_settings, serverController);
QJsonObject containerConfig = m_containersModel->getContainerConfig(container);
ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex);
QSharedPointer<ServerController> serverController(new ServerController(m_settings));
QFutureWatcher<ErrorCode> watcher;
auto isProtocolConfigExists = [](const QJsonObject &containerConfig, const DockerContainer container) {
for (Proto protocol : ContainerProps::protocolsForContainer(container)) {
QString protocolConfig =
containerConfig.value(ProtocolProps::protoToString(protocol)).toObject().value(config_key::last_config).toString();
QFuture<ErrorCode> future = QtConcurrent::run([this, container, &credentials, &containerConfig, &serverController]() {
ErrorCode errorCode = ErrorCode::NoError;
auto isProtocolConfigExists = [](const QJsonObject &containerConfig, const DockerContainer container) {
for (Proto protocol : ContainerProps::protocolsForContainer(container)) {
QString protocolConfig =
containerConfig.value(ProtocolProps::protoToString(protocol)).toObject().value(config_key::last_config).toString();
if (protocolConfig.isEmpty()) {
return false;
}
}
return true;
};
if (!isProtocolConfigExists(containerConfig, container)) {
VpnConfigurationsController vpnConfigurationController(m_settings, serverController);
errorCode = vpnConfigurationController.createProtocolConfigForContainer(credentials, container, containerConfig);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
m_serversModel->updateContainerConfig(container, containerConfig);
errorCode = m_clientManagementModel->appendClient(container, credentials, containerConfig,
QString("Admin [%1]").arg(QSysInfo::prettyProductName()), serverController);
if (errorCode != ErrorCode::NoError) {
return errorCode;
if (protocolConfig.isEmpty()) {
return false;
}
}
return errorCode;
});
return true;
};
QEventLoop wait;
connect(&watcher, &QFutureWatcher<ErrorCode>::finished, &wait, &QEventLoop::quit);
watcher.setFuture(future);
wait.exec();
ErrorCode errorCode = watcher.result();
if (errorCode != ErrorCode::NoError) {
emit installationErrorOccurred(errorCode);
return false;
if (isProtocolConfigExists(containerConfig, container)) {
emit configValidated(true);
return;
}
return true;
struct ValidationResult {
ErrorCode errorCode = ErrorCode::NoError;
QJsonObject containerConfig;
};
QFuture<ValidationResult> future =
QtConcurrent::run([settings = m_settings, serverController, credentials, containerConfig, container]() mutable {
ValidationResult result;
result.containerConfig = containerConfig;
VpnConfigurationsController vpnConfigurationController(settings, serverController);
result.errorCode = vpnConfigurationController.createProtocolConfigForContainer(credentials, container,
result.containerConfig);
return result;
});
auto *watcher = new QFutureWatcher<ValidationResult>(this);
connect(watcher, &QFutureWatcher<ValidationResult>::finished, this,
[this, watcher, container, credentials, serverController]() {
auto result = watcher->result();
watcher->deleteLater();
if (result.errorCode != ErrorCode::NoError) {
emit installationErrorOccurred(result.errorCode);
emit configValidated(false);
return;
}
m_serversModel->updateContainerConfig(container, result.containerConfig);
ErrorCode appendError = m_clientManagementModel->appendClient(
container, credentials, result.containerConfig,
QString("Admin [%1]").arg(QSysInfo::prettyProductName()), serverController);
if (appendError != ErrorCode::NoError) {
emit installationErrorOccurred(appendError);
emit configValidated(false);
return;
}
emit configValidated(true);
});
watcher->setFuture(future);
}
bool InstallController::isUpdateDockerContainerRequired(const DockerContainer container, const QJsonObject &oldConfig,
@@ -1070,7 +1085,7 @@ bool InstallController::isUpdateDockerContainerRequired(const DockerContainer co
const QJsonObject &oldProtoConfig = oldConfig.value(ProtocolProps::protoToString(mainProto)).toObject();
const QJsonObject &newProtoConfig = newConfig.value(ProtocolProps::protoToString(mainProto)).toObject();
if (container == DockerContainer::Awg2) {
if (ContainerProps::isAwgContainer(container)) {
const AwgConfig oldConfig(oldProtoConfig);
const AwgConfig newConfig(newProtoConfig);

View File

@@ -50,9 +50,10 @@ public slots:
void addEmptyServer();
bool isConfigValid();
void validateConfig();
signals:
void configValidated(bool isValid);
void installContainerFinished(const QString &finishMessage, bool isServiceInstall);
void installServerFinished(const QString &finishMessage);

View File

@@ -45,6 +45,8 @@ SettingsController::SettingsController(const QSharedPointer<ServersModel> &serve
emit safeAreaBottomMarginChanged();
emit safeAreaTopMarginChanged();
});
connect(AndroidController::instance(), &AndroidController::activityPaused, this, &SettingsController::activityPaused);
connect(AndroidController::instance(), &AndroidController::activityResumed, this, &SettingsController::activityResumed);
#endif
m_isDevModeEnabled = m_settings->isDevGatewayEnv();
@@ -178,12 +180,11 @@ void SettingsController::backupAppConfig(const QString &fileName)
void SettingsController::restoreAppConfig(const QString &fileName)
{
QFile file(fileName);
file.open(QIODevice::ReadOnly);
QByteArray data = file.readAll();
QByteArray data;
if (!SystemController::readFile(fileName, data)) {
emit changeSettingsErrorOccurred(tr("Can't open file: %1").arg(fileName));
return;
}
restoreAppConfigFromData(data);
}

View File

@@ -141,6 +141,9 @@ signals:
void safeAreaTopMarginChanged();
void safeAreaBottomMarginChanged();
void activityPaused();
void activityResumed();
void isHomeAdLabelVisibleChanged(bool visible);
void startMinimizedChanged();

View File

@@ -7,10 +7,8 @@
#include "systemController.h"
#include "core/networkUtilities.h"
SitesController::SitesController(const std::shared_ptr<Settings> &settings,
const QSharedPointer<VpnConnection> &vpnConnection,
const QSharedPointer<SitesModel> &sitesModel, QObject *parent)
: QObject(parent), m_settings(settings), m_vpnConnection(vpnConnection), m_sitesModel(sitesModel)
SitesController::SitesController(const std::shared_ptr<Settings> &settings, const QSharedPointer<SitesModel> &sitesModel, QObject *parent)
: QObject(parent), m_settings(settings), m_sitesModel(sitesModel)
{
}
@@ -34,32 +32,20 @@ void SitesController::addSite(QString hostname)
hostname = hostname.split("/", Qt::SkipEmptyParts).first();
}
const auto &processSite = [this](const QString &hostname, const QString &ip) {
m_sitesModel->addSite(hostname, ip);
if (!ip.isEmpty()) {
QMetaObject::invokeMethod(m_vpnConnection.get(), "addRoutes", Qt::QueuedConnection,
Q_ARG(QStringList, QStringList() << ip));
} else if (NetworkUtilities::ipAddressWithSubnetRegExp().exactMatch(hostname)) {
QMetaObject::invokeMethod(m_vpnConnection.get(), "addRoutes", Qt::QueuedConnection,
Q_ARG(QStringList, QStringList() << hostname));
}
};
const auto &resolveCallback = [this, processSite](const QHostInfo &hostInfo) {
const auto &resolveCallback = [this](const QHostInfo &hostInfo) {
const QList<QHostAddress> &addresses = hostInfo.addresses();
for (const QHostAddress &addr : hostInfo.addresses()) {
if (addr.protocol() == QAbstractSocket::NetworkLayerProtocol::IPv4Protocol) {
processSite(hostInfo.hostName(), addr.toString());
m_sitesModel->addSite(hostInfo.hostName(), addr.toString());
break;
}
}
};
if (NetworkUtilities::ipAddressWithSubnetRegExp().exactMatch(hostname)) {
processSite(hostname, "");
m_sitesModel->addSite(hostname, "");
} else {
processSite(hostname, "");
m_sitesModel->addSite(hostname, "");
QHostInfo::lookupHost(hostname, this, resolveCallback);
}
@@ -72,9 +58,6 @@ void SitesController::removeSite(int index)
auto hostname = m_sitesModel->data(modelIndex, SitesModel::Roles::UrlRole).toString();
m_sitesModel->removeSite(modelIndex);
QMetaObject::invokeMethod(m_vpnConnection.get(), "deleteRoutes", Qt::QueuedConnection,
Q_ARG(QStringList, QStringList() << hostname));
emit finished(tr("Site removed: %1").arg(hostname));
}
@@ -128,8 +111,6 @@ void SitesController::importSites(const QString &fileName, bool replaceExisting)
m_sitesModel->addSites(sites, replaceExisting);
QMetaObject::invokeMethod(m_vpnConnection.get(), "addRoutes", Qt::QueuedConnection, Q_ARG(QStringList, ips));
emit finished(tr("Import completed"));
}

View File

@@ -11,9 +11,8 @@ class SitesController : public QObject
{
Q_OBJECT
public:
explicit SitesController(const std::shared_ptr<Settings> &settings,
const QSharedPointer<VpnConnection> &vpnConnection,
const QSharedPointer<SitesModel> &sitesModel, QObject *parent = nullptr);
explicit SitesController(const std::shared_ptr<Settings> &settings, const QSharedPointer<SitesModel> &sitesModel,
QObject *parent = nullptr);
public slots:
void addSite(QString hostname);
@@ -31,8 +30,6 @@ signals:
private:
std::shared_ptr<Settings> m_settings;
QSharedPointer<VpnConnection> m_vpnConnection;
QSharedPointer<SitesModel> m_sitesModel;
};

View File

@@ -1,5 +1,6 @@
#include "apiAccountInfoModel.h"
#include <QDateTime>
#include <QJsonObject>
#include "core/api/apiUtils.h"
@@ -32,7 +33,7 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
}
return apiUtils::isSubscriptionExpired(m_accountInfoData.subscriptionEndDate) ? tr("<p><a style=\"color: #EB5757;\">Inactive</a>")
: tr("Active");
: tr("<p><a style=\"color: #28c840;\">Active</a>");
}
case EndDateRole: {
if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) {
@@ -52,7 +53,9 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
}
case IsComponentVisibleRole: {
return m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2
|| m_accountInfoData.configType == apiDefs::ConfigType::ExternalPremium;
|| m_accountInfoData.configType == apiDefs::ConfigType::AmneziaTrialV2
|| m_accountInfoData.configType == apiDefs::ConfigType::ExternalPremium
|| m_accountInfoData.configType == apiDefs::ConfigType::ExternalTrial;
}
case HasExpiredWorkerRole: {
for (int i = 0; i < m_issuedConfigsInfo.size(); i++) {
@@ -73,6 +76,18 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
}
return false;
}
case IsSubscriptionExpiredRole: {
if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) return false;
if (m_accountInfoData.subscriptionEndDate.isEmpty()) return false;
return apiUtils::isSubscriptionExpired(m_accountInfoData.subscriptionEndDate);
}
case IsSubscriptionExpiringSoonRole: {
if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) return false;
if (m_accountInfoData.subscriptionEndDate.isEmpty()) return false;
if (apiUtils::isSubscriptionExpired(m_accountInfoData.subscriptionEndDate)) return false;
QDateTime endDate = QDateTime::fromString(m_accountInfoData.subscriptionEndDate, Qt::ISODateWithMs);
return endDate <= QDateTime::currentDateTimeUtc().addDays(10);
}
}
return QVariant();
@@ -164,6 +179,8 @@ QHash<int, QByteArray> ApiAccountInfoModel::roleNames() const
roles[IsComponentVisibleRole] = "isComponentVisible";
roles[HasExpiredWorkerRole] = "hasExpiredWorker";
roles[IsProtocolSelectionSupportedRole] = "isProtocolSelectionSupported";
roles[IsSubscriptionExpiredRole] = "isSubscriptionExpired";
roles[IsSubscriptionExpiringSoonRole] = "isSubscriptionExpiringSoon";
return roles;
}

View File

@@ -19,7 +19,9 @@ public:
EndDateRole,
IsComponentVisibleRole,
HasExpiredWorkerRole,
IsProtocolSelectionSupportedRole
IsProtocolSelectionSupportedRole,
IsSubscriptionExpiredRole,
IsSubscriptionExpiringSoonRole
};
explicit ApiAccountInfoModel(QObject *parent = nullptr);
@@ -31,7 +33,6 @@ public:
public slots:
void updateModel(const QJsonObject &accountInfoObject, const QJsonObject &serverConfig);
QVariant data(const QString &roleString);
QJsonArray getAvailableCountries();
QJsonArray getIssuedConfigsInfo();

View File

@@ -41,6 +41,7 @@ namespace
{
constexpr char amneziaFree[] = "amnezia-free";
constexpr char amneziaPremium[] = "amnezia-premium";
constexpr char amneziaTrial[] = "amnezia-trial";
}
}
@@ -69,7 +70,7 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
}
case CardDescriptionRole: {
auto speed = apiServiceData.serviceInfo.speed;
if (serviceType == serviceType::amneziaPremium) {
if (serviceType == serviceType::amneziaPremium || serviceType == serviceType::amneziaTrial) {
return apiServiceData.serviceInfo.cardDescription.arg(speed);
} else if (serviceType == serviceType::amneziaFree) {
QString description = apiServiceData.serviceInfo.cardDescription;
@@ -124,8 +125,10 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
case OrderRole: {
if (serviceType == serviceType::amneziaPremium) {
return 0;
} else if (serviceType == serviceType::amneziaFree) {
} else if (serviceType == serviceType::amneziaTrial) {
return 1;
} else if (serviceType == serviceType::amneziaFree) {
return 2;
}
}
}

View File

@@ -141,10 +141,12 @@ void AwgConfigModel::updateModel(const QJsonObject &config)
serverProtocolConfig.value(config_key::initPacketJunkSize).toString(protocols::awg::defaultInitPacketJunkSize);
m_serverProtocolConfig[config_key::responsePacketJunkSize] =
serverProtocolConfig.value(config_key::responsePacketJunkSize).toString(protocols::awg::defaultResponsePacketJunkSize);
m_serverProtocolConfig[config_key::cookieReplyPacketJunkSize] =
serverProtocolConfig.value(config_key::cookieReplyPacketJunkSize).toString(protocols::awg::defaultCookieReplyPacketJunkSize);
m_serverProtocolConfig[config_key::transportPacketJunkSize] =
serverProtocolConfig.value(config_key::transportPacketJunkSize).toString(protocols::awg::defaultTransportPacketJunkSize);
if (protocolVersion == protocols::awg::awgV2) {
m_serverProtocolConfig[config_key::cookieReplyPacketJunkSize] =
serverProtocolConfig.value(config_key::cookieReplyPacketJunkSize).toString(protocols::awg::defaultCookieReplyPacketJunkSize);
m_serverProtocolConfig[config_key::transportPacketJunkSize] =
serverProtocolConfig.value(config_key::transportPacketJunkSize).toString(protocols::awg::defaultTransportPacketJunkSize);
}
m_serverProtocolConfig[config_key::initPacketMagicHeader] =
serverProtocolConfig.value(config_key::initPacketMagicHeader).toString(protocols::awg::defaultInitPacketMagicHeader);
m_serverProtocolConfig[config_key::responsePacketMagicHeader] =

View File

@@ -179,6 +179,20 @@ QVariant ServersModel::data(const QModelIndex &index, int role) const
case AdEndpointRole: {
return apiConfig.value(apiDefs::key::serviceInfo).toObject().value(apiDefs::key::adEndpoint).toString();
}
case IsSubscriptionExpiredRole: {
if (configVersion != apiDefs::ConfigSource::AmneziaGateway) return false;
QString endDate = apiConfig.value(apiDefs::key::subscriptionEndDate).toString();
if (endDate.isEmpty()) return false;
return apiUtils::isSubscriptionExpired(endDate);
}
case IsSubscriptionExpiringSoonRole: {
if (configVersion != apiDefs::ConfigSource::AmneziaGateway) return false;
QString endDate = apiConfig.value(apiDefs::key::subscriptionEndDate).toString();
if (endDate.isEmpty()) return false;
if (apiUtils::isSubscriptionExpired(endDate)) return false;
QDateTime endDateTime = QDateTime::fromString(endDate, Qt::ISODateWithMs);
return endDateTime <= QDateTime::currentDateTimeUtc().addDays(10);
}
}
return QVariant();
@@ -443,6 +457,9 @@ QHash<int, QByteArray> ServersModel::roleNames() const
roles[AdDescriptionRole] = "adDescription";
roles[AdEndpointRole] = "adEndpoint";
roles[IsSubscriptionExpiredRole] = "isSubscriptionExpired";
roles[IsSubscriptionExpiringSoonRole] = "isSubscriptionExpiringSoon";
return roles;
}

View File

@@ -52,6 +52,9 @@ public:
AdDescriptionRole,
AdEndpointRole,
IsSubscriptionExpiredRole,
IsSubscriptionExpiringSoonRole,
HasAmneziaDns
};

View File

@@ -126,6 +126,18 @@ ListViewType {
}
}
CaptionTextType {
visible: isServerFromGatewayApi && (isSubscriptionExpired || isSubscriptionExpiringSoon)
Layout.fillWidth: true
Layout.leftMargin: 64
Layout.bottomMargin: 8
text: isSubscriptionExpired ? qsTr("Subscription expired. Please renew.") : qsTr("Subscription expiring soon.")
color: isSubscriptionExpired ? AmneziaStyle.color.vibrantRed : AmneziaStyle.color.goldenApricot
wrapMode: Text.WordWrap
}
DividerType {
Layout.fillWidth: true
Layout.leftMargin: 0

View File

@@ -0,0 +1,93 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import PageEnum 1.0
import Style 1.0
import "../Controls2"
import "../Controls2/TextTypes"
DrawerType2 {
id: root
expandedStateContent: ColumnLayout {
id: content
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
spacing: 0
onImplicitHeightChanged: {
root.expandedHeight = content.implicitHeight + 32 + SettingsController.safeAreaBottomMargin
}
Item {
Layout.fillWidth: true
Layout.topMargin: 24
Layout.rightMargin: 16
Layout.leftMargin: 16
implicitHeight: titleText.implicitHeight
Header2TextType {
id: titleText
anchors.left: parent.left
anchors.right: parent.right
text: qsTr("Amnezia Premium subscription has expired")
horizontalAlignment: Text.AlignLeft
}
}
ParagraphTextType {
Layout.fillWidth: true
Layout.topMargin: 8
Layout.rightMargin: 16
Layout.leftMargin: 16
text: qsTr("Renew your subscription to continue using VPN")
horizontalAlignment: Text.AlignLeft
}
BasicButtonType {
Layout.fillWidth: true
Layout.topMargin: 16
Layout.rightMargin: 16
Layout.leftMargin: 16
text: qsTr("Renew")
defaultColor: AmneziaStyle.color.paleGray
hoveredColor: AmneziaStyle.color.lightGray
pressedColor: AmneziaStyle.color.mutedGray
textColor: AmneziaStyle.color.midnightBlack
clickedFunc: function() {
ApiSettingsController.getRenewalLink()
}
}
BasicButtonType {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 8
Layout.bottomMargin: 8
implicitHeight: 25
defaultColor: AmneziaStyle.color.transparent
hoveredColor: AmneziaStyle.color.translucentWhite
pressedColor: AmneziaStyle.color.sheerWhite
textColor: AmneziaStyle.color.goldenApricot
text: qsTr("Support")
clickedFunc: function() {
root.closeTriggered()
PageController.goToPage(PageEnum.PageSettingsApiSupport)
}
}
}
}

View File

@@ -1,6 +1,7 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Style 1.0
@@ -37,6 +38,7 @@ Item {
property int borderFocusedWidth: 1
property string rightImageColor: AmneziaStyle.color.paleGray
property string leftImageColor: ""
property bool descriptionOnTop: false
property bool hideDescription: true
@@ -140,6 +142,14 @@ Item {
anchors.centerIn: parent
source: leftImageSource
visible: leftImageColor === ""
}
ColorOverlay {
anchors.fill: leftImage
source: leftImage
color: leftImageColor
visible: leftImageColor !== ""
}
}

View File

@@ -8,9 +8,10 @@ Item {
id: root
property StackView stackView: StackView.view
property bool enableTimer: true
onVisibleChanged: {
if (visible) {
if (visible && enableTimer) {
timer.start()
}
}
@@ -24,6 +25,6 @@ Item {
FocusController.setFocusOnDefaultItem()
}
repeat: false // Stop the timer after one trigger
running: true // Start the timer
running: enableTimer // Start the timer
}
}

View File

@@ -330,6 +330,8 @@ PageType {
AwgTextField {
id: cookieReplyPacketJunkSizeTextField
visible: isAwg2
Layout.leftMargin: 16
Layout.rightMargin: 16
@@ -342,6 +344,8 @@ PageType {
AwgTextField {
id: transportPacketJunkSizeTextField
visible: isAwg2
Layout.leftMargin: 16
Layout.rightMargin: 16

View File

@@ -18,12 +18,23 @@ PageType {
id: root
property var processedServer
property bool subscriptionExpired: false
property bool subscriptionExpiringSoon: false
function updateSubscriptionState() {
root.subscriptionExpired = ServersModel.getProcessedServerData("isSubscriptionExpired")
root.subscriptionExpiringSoon = ServersModel.getProcessedServerData("isSubscriptionExpiringSoon")
}
Component.onCompleted: {
root.updateSubscriptionState()
}
Connections {
target: ServersModel
function onProcessedServerChanged() {
root.processedServer = proxyServersModel.get(0)
root.updateSubscriptionState()
}
}
@@ -76,12 +87,11 @@ PageType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 10
Layout.bottomMargin: 4
actionButtonImage: "qrc:/images/controls/settings.svg"
headerText: root.processedServer.name
descriptionText: qsTr("Location for connection")
actionButtonFunction: function() {
PageController.showBusyIndicator(true)
@@ -94,6 +104,50 @@ PageType {
PageController.goToPage(PageEnum.PageSettingsApiServerInfo)
}
}
CaptionTextType {
visible: root.subscriptionExpired || root.subscriptionExpiringSoon
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 4
text: root.subscriptionExpired ? qsTr("Subscription expired") : qsTr("Subscription expiring soon")
color: root.subscriptionExpired ? AmneziaStyle.color.vibrantRed : AmneziaStyle.color.goldenApricot
}
BasicButtonType {
visible: root.subscriptionExpired || root.subscriptionExpiringSoon
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
Layout.bottomMargin: 4
defaultColor: AmneziaStyle.color.paleGray
hoveredColor: AmneziaStyle.color.lightGray
pressedColor: AmneziaStyle.color.mutedGray
textColor: AmneziaStyle.color.midnightBlack
text: qsTr("Renew subscription")
clickedFunc: function() {
ApiSettingsController.getRenewalLink()
}
}
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: (root.subscriptionExpired || root.subscriptionExpiringSoon) ? 8 : 4
Layout.bottomMargin: 8
text: qsTr("Location for connection")
color: AmneziaStyle.color.mutedGray
}
}
delegate: ColumnLayout {

View File

@@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import Qt5Compat.GraphicalEffects
import SortFilterProxyModel 0.2
@@ -52,6 +53,26 @@ PageType {
property var processedServer
property bool isSubscriptionExpired: false
property bool isSubscriptionExpiringSoon: false
function updateSubscriptionState() {
root.isSubscriptionExpired = ApiAccountInfoModel.data("isSubscriptionExpired")
root.isSubscriptionExpiringSoon = ApiAccountInfoModel.data("isSubscriptionExpiringSoon")
}
Component.onCompleted: {
root.updateSubscriptionState()
}
Connections {
target: ApiAccountInfoModel
function onModelReset() {
root.updateSubscriptionState()
}
}
Connections {
target: ServersModel
@@ -108,12 +129,66 @@ PageType {
actionButtonImage: "qrc:/images/controls/edit-3.svg"
headerText: root.processedServer.name
descriptionText: ApiAccountInfoModel.data("serviceDescription")
actionButtonFunction: function() {
serverNameEditDrawer.openTriggered()
}
}
Text {
visible: root.isSubscriptionExpired || root.isSubscriptionExpiringSoon
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 4
text: root.isSubscriptionExpired
? qsTr("Subscription expired")
: qsTr("Subscription expiring soon")
color: root.isSubscriptionExpired
? AmneziaStyle.color.vibrantRed
: AmneziaStyle.color.goldenApricot
font.pixelSize: 14
font.weight: Font.Medium
wrapMode: Text.WordWrap
}
ParagraphTextType {
visible: ApiAccountInfoModel.data("serviceDescription") !== ""
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 16
Layout.bottomMargin: root.isSubscriptionExpired || root.isSubscriptionExpiringSoon ? 0 : 10
text: ApiAccountInfoModel.data("serviceDescription")
color: AmneziaStyle.color.mutedGray
}
BasicButtonType {
visible: root.isSubscriptionExpired || root.isSubscriptionExpiringSoon
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
Layout.bottomMargin: 8
text: qsTr("Renew subscription")
defaultColor: AmneziaStyle.color.paleGray
hoveredColor: AmneziaStyle.color.lightGray
pressedColor: AmneziaStyle.color.mutedGray
textColor: AmneziaStyle.color.midnightBlack
clickedFunc: function() {
ApiSettingsController.getRenewalLink()
}
}
}
delegate: ColumnLayout {
@@ -151,6 +226,54 @@ PageType {
readonly property bool isVisibleForAmneziaFree: ApiAccountInfoModel.data("isComponentVisible")
Item {
visible: !root.isSubscriptionExpired && !root.isSubscriptionExpiringSoon
Layout.fillWidth: true
implicitHeight: renewRow.implicitHeight + 32
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: ApiSettingsController.getRenewalLink()
}
Row {
id: renewRow
anchors.centerIn: parent
spacing: 12
Item {
width: renewIcon.implicitWidth
height: renewIcon.implicitHeight
anchors.verticalCenter: parent.verticalCenter
Image {
id: renewIcon
source: "qrc:/images/controls/refresh-cw.svg"
}
ColorOverlay {
anchors.fill: renewIcon
source: renewIcon
color: AmneziaStyle.color.goldenApricot
}
}
Text {
text: qsTr("Renew subscription")
color: AmneziaStyle.color.goldenApricot
font.pixelSize: 18
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
}
DividerType {
visible: !root.isSubscriptionExpired && !root.isSubscriptionExpiringSoon
}
SwitcherType {
id: switcher
@@ -177,10 +300,14 @@ PageType {
}
}
DividerType {
visible: footer.isVisibleForAmneziaFree
}
WarningType {
id: warning
Layout.topMargin: 32
Layout.topMargin: 24
Layout.rightMargin: 16
Layout.leftMargin: 16
Layout.fillWidth: true
@@ -204,7 +331,7 @@ PageType {
id: vpnKey
Layout.fillWidth: true
Layout.topMargin: warning.visible ? 16 : 32
Layout.topMargin: warning.visible ? 16 : 0
visible: footer.isVisibleForAmneziaFree
@@ -396,9 +523,7 @@ PageType {
PageController.showNotificationMessage(qsTr("Cannot remove server during active connection"))
} else {
PageController.showBusyIndicator(true)
if (ApiConfigsController.deactivateDevice(true)) {
InstallController.removeProcessedServer()
}
InstallController.removeProcessedServer()
PageController.showBusyIndicator(false)
}
}

View File

@@ -178,7 +178,7 @@ PageType {
Layout.margins: 16
text: qsTr("News Notification")
descriptionText: qsTr("Show notification icon when has unread news")
descriptionText: qsTr("Show a notification icon for unread news")
checked: SettingsController.isNewsNotificationsEnabled()
onToggled: function() {

View File

@@ -1,226 +1,226 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import PageEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
PageType {
id: root
BackButtonType {
id: backButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onFocusChanged: {
if (this.activeFocus) {
listView.positionViewAtBeginning()
}
}
}
ListViewType {
id: listView
anchors.top: backButton.bottom
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.left: parent.left
header: ColumnLayout {
width: listView.width
BaseHeaderType {
Layout.fillWidth: true
Layout.topMargin: 8
Layout.rightMargin: 16
Layout.leftMargin: 16
Layout.bottomMargin: 32
headerText: ApiServicesModel.getSelectedServiceData("name")
descriptionText: ApiServicesModel.getSelectedServiceData("serviceDescription")
}
}
model: inputFields
spacing: 0
delegate: ColumnLayout {
width: listView.width
LabelWithImageType {
Layout.fillWidth: true
Layout.margins: 16
imageSource: imagePath
leftText: lText
rightText: rText
visible: isVisible
}
}
footer: ColumnLayout {
width: listView.width
spacing: 0
ParagraphTextType {
Layout.fillWidth: true
Layout.rightMargin: 16
Layout.leftMargin: 16
onLinkActivated: function(link) {
Qt.openUrlExternally(link)
}
textFormat: Text.RichText
text: {
var text = ApiServicesModel.getSelectedServiceData("features")
return text.replace("%1", LanguageModel.getCurrentSiteUrl("free")).replace("/free", "") // todo link should come from gateway
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
ParagraphTextType {
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
visible: (Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium"
horizontalAlignment: Text.AlignHCenter
textFormat: Text.PlainText
color: AmneziaStyle.color.mutedGray
font.pixelSize: 12
text: qsTr("Charged to your Apple ID at confirmation. Renews automatically unless auto-renew is turned off at least 24 hours before period end. Manage in Apple ID settings.")
}
BasicButtonType {
id: continueButton
Layout.fillWidth: true
Layout.topMargin: 32
Layout.bottomMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
text: ApiServicesModel.getSelectedServiceType() === "amnezia-premium" ? qsTr("Subscribe Now") : qsTr("Connect")
clickedFunc: function() {
PageController.showBusyIndicator(true)
var result = ApiConfigsController.importService()
PageController.showBusyIndicator(false)
if (!result) {
var endpoint = ApiServicesModel.getStoreEndpoint()
Qt.openUrlExternally(endpoint)
PageController.closePage()
PageController.closePage()
}
}
}
ParagraphTextType {
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 32
visible: (Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium"
horizontalAlignment: Text.AlignHCenter
textFormat: Text.RichText
color: AmneziaStyle.color.mutedGray
font.pixelSize: 12
text: {
var termsUrl = "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/"
var privacyUrl = LanguageModel.getCurrentSiteUrl("policy")
return qsTr("By continuing, you agree to the <a href=\"%1\" style=\"color: #FBB26A;\">Terms of Use</a> and <a href=\"%2\" style=\"color: #FBB26A;\">Privacy Policy</a>").arg(termsUrl).arg(privacyUrl)
}
onLinkActivated: function(link) {
Qt.openUrlExternally(link)
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
}
}
property list<QtObject> inputFields: [
region,
price,
timeLimit,
speed,
features
]
QtObject {
id: region
readonly property string imagePath: "qrc:/images/controls/map-pin.svg"
readonly property string lText: qsTr("For the region")
readonly property string rText: ApiServicesModel.getSelectedServiceData("region")
property bool isVisible: true
}
QtObject {
id: price
readonly property string imagePath: "qrc:/images/controls/tag.svg"
readonly property string lText: qsTr("Price")
readonly property string rText: ApiServicesModel.getSelectedServiceData("price")
property bool isVisible: true
}
QtObject {
id: timeLimit
readonly property string imagePath: "qrc:/images/controls/history.svg"
readonly property string lText: qsTr("Work period")
readonly property string rText: ApiServicesModel.getSelectedServiceData("timeLimit")
property bool isVisible: rText !== ""
}
QtObject {
id: speed
readonly property string imagePath: "qrc:/images/controls/gauge.svg"
readonly property string lText: qsTr("Speed")
readonly property string rText: ApiServicesModel.getSelectedServiceData("speed")
property bool isVisible: true
}
QtObject {
id: features
readonly property string imagePath: "qrc:/images/controls/info.svg"
readonly property string lText: qsTr("Features")
readonly property string rText: ""
property bool isVisible: true
}
}
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import PageEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
PageType {
id: root
BackButtonType {
id: backButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onFocusChanged: {
if (this.activeFocus) {
listView.positionViewAtBeginning()
}
}
}
ListViewType {
id: listView
anchors.top: backButton.bottom
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.left: parent.left
header: ColumnLayout {
width: listView.width
BaseHeaderType {
Layout.fillWidth: true
Layout.topMargin: 8
Layout.rightMargin: 16
Layout.leftMargin: 16
Layout.bottomMargin: 32
headerText: ApiServicesModel.getSelectedServiceData("name")
descriptionText: ApiServicesModel.getSelectedServiceData("serviceDescription")
}
}
model: inputFields
spacing: 0
delegate: ColumnLayout {
width: listView.width
LabelWithImageType {
Layout.fillWidth: true
Layout.margins: 16
imageSource: imagePath
leftText: lText
rightText: rText
visible: isVisible
}
}
footer: ColumnLayout {
width: listView.width
spacing: 0
ParagraphTextType {
Layout.fillWidth: true
Layout.rightMargin: 16
Layout.leftMargin: 16
onLinkActivated: function(link) {
Qt.openUrlExternally(link)
}
textFormat: Text.RichText
text: {
var text = ApiServicesModel.getSelectedServiceData("features")
return text.replace("%1", LanguageModel.getCurrentSiteUrl("free")).replace("/free", "") // todo link should come from gateway
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
ParagraphTextType {
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
visible: (Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium"
horizontalAlignment: Text.AlignHCenter
textFormat: Text.PlainText
color: AmneziaStyle.color.mutedGray
font.pixelSize: 12
text: qsTr("Charged to your Apple ID at confirmation. Renews automatically unless auto-renew is turned off at least 24 hours before period end. Manage in Apple ID settings.")
}
BasicButtonType {
id: continueButton
Layout.fillWidth: true
Layout.topMargin: 32
Layout.bottomMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
text: ApiServicesModel.getSelectedServiceType() === "amnezia-premium" ? qsTr("Subscribe Now") : (ApiServicesModel.getSelectedServiceType() === "amnezia-trial" ? qsTr("Try Trial") : qsTr("Connect"))
clickedFunc: function() {
PageController.showBusyIndicator(true)
var result = ApiConfigsController.importService()
PageController.showBusyIndicator(false)
if (!result) {
var endpoint = ApiServicesModel.getStoreEndpoint()
Qt.openUrlExternally(endpoint)
PageController.closePage()
PageController.closePage()
}
}
}
ParagraphTextType {
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 32
visible: (Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium"
horizontalAlignment: Text.AlignHCenter
textFormat: Text.RichText
color: AmneziaStyle.color.mutedGray
font.pixelSize: 12
text: {
var termsUrl = "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/"
var privacyUrl = LanguageModel.getCurrentSiteUrl("policy")
return qsTr("By continuing, you agree to the <a href=\"%1\" style=\"color: #FBB26A;\">Terms of Use</a> and <a href=\"%2\" style=\"color: #FBB26A;\">Privacy Policy</a>").arg(termsUrl).arg(privacyUrl)
}
onLinkActivated: function(link) {
Qt.openUrlExternally(link)
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
}
}
property list<QtObject> inputFields: [
region,
price,
timeLimit,
speed,
features
]
QtObject {
id: region
readonly property string imagePath: "qrc:/images/controls/map-pin.svg"
readonly property string lText: qsTr("For the region")
readonly property string rText: ApiServicesModel.getSelectedServiceData("region")
property bool isVisible: true
}
QtObject {
id: price
readonly property string imagePath: "qrc:/images/controls/tag.svg"
readonly property string lText: qsTr("Price")
readonly property string rText: ApiServicesModel.getSelectedServiceData("price")
property bool isVisible: true
}
QtObject {
id: timeLimit
readonly property string imagePath: "qrc:/images/controls/history.svg"
readonly property string lText: qsTr("Work period")
readonly property string rText: ApiServicesModel.getSelectedServiceData("timeLimit")
property bool isVisible: rText !== ""
}
QtObject {
id: speed
readonly property string imagePath: "qrc:/images/controls/gauge.svg"
readonly property string lText: qsTr("Speed")
readonly property string rText: ApiServicesModel.getSelectedServiceData("speed")
property bool isVisible: true
}
QtObject {
id: features
readonly property string imagePath: "qrc:/images/controls/info.svg"
readonly property string lText: qsTr("Features")
readonly property string rText: ""
property bool isVisible: true
}
}

View File

@@ -79,11 +79,23 @@ PageType {
}
textField.onTextChanged: {
if (headerText == qsTr("Password or SSH private key")) {
if (headerText === qsTr("Password or SSH private key")) {
buttonImageSource = textField.text !== "" ? imageSource : ""
}
}
}
WarningType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
visible: title === qsTr("Password or SSH private key")
backGroundColor: AmneziaStyle.color.translucentWhite
iconPath: "qrc:/images/controls/alert-circle.svg"
textString: qsTr("SSH key requirements: supported ED25519 or RSA in PEM. Paste the private key including BEGIN/END lines. If your key doesnt work, generate a compatible one.")
}
}
footer: ColumnLayout {

View File

@@ -13,6 +13,7 @@ import "../Components"
PageType {
id: root
enableTimer: (SettingsController.isOnTv()) ? false : true
ColumnLayout {
id: content
@@ -45,4 +46,22 @@ PageType {
}
}
}
Timer {
interval: 250
running: SettingsController.isOnTv()
repeat: true
onTriggered: {
startButton.forceActiveFocus()
if (startButton.activeFocus) {
running = false
}
}
}
onVisibleChanged: {
if (visible && SettingsController.isOnTv()) {
startButton.forceActiveFocus()
}
}
}

View File

@@ -505,7 +505,13 @@ PageType {
exportTypeSelector.currentIndex = 0
}
selectedIndex = exportTypeSelector.currentIndex
exportTypeSelector.text = selectedText
if (model.length > 0 && model[selectedIndex] && model[selectedIndex].name !== undefined) {
exportTypeSelectorListView.selectedText = model[selectedIndex].name
exportTypeSelector.text = model[selectedIndex].name
} else {
exportTypeSelectorListView.selectedText = ""
exportTypeSelector.text = ""
}
}
rootWidth: root.width

View File

@@ -278,7 +278,6 @@ PageType {
}
Keys.onPressed: function(event) {
console.debug(">>>> ", event.key, " Event is caught by StartPage")
switch (event.key) {
case Qt.Key_Tab:
case Qt.Key_Down:
@@ -304,7 +303,7 @@ PageType {
anchors.right: parent.right
anchors.left: parent.left
anchors.bottom: parent.bottom
// Also adjust TabBar position when keyboard appears (Android 14+ workaround)
anchors.bottomMargin: SettingsController.imeHeight

View File

@@ -21,15 +21,27 @@ Window {
function onStateChanged() {
if (Qt.platform.os === "android") {
if (Qt.application.state === Qt.ApplicationActive) {
root.visible = true
refreshTimer.restart()
} else if (Qt.application.state === Qt.ApplicationSuspended ||
Qt.application.state === Qt.ApplicationInactive) {
console.log("QML: Application going to background, state:", Qt.application.state)
}
}
}
}
// Hide the window immediately when Android Activity.onPause() fires so that
// Qt's render loop stops before the EGL surface is disconnected. This
// prevents "QRhiGles2: Failed to make context current" and the resulting
// black screen that appears after swiping home and returning.
Connections {
target: SettingsController
function onActivityPaused() {
if (Qt.platform.os === "android") root.visible = false
}
function onActivityResumed() {
if (Qt.platform.os === "android") root.visible = true
}
}
Timer {
id: refreshTimer
interval: 150
@@ -56,6 +68,11 @@ Window {
PageController.closeWindow()
}
onSceneGraphError: function(error, message) {
// Prevent qFatal crash on Android when EGL context is lost
console.warn("Scene graph error:", error, message)
}
title: "AmneziaVPN"
Item { // This item is needed for focus handling
@@ -271,6 +288,34 @@ Window {
}
}
Item {
objectName: "subscriptionExpiredDrawerItem"
anchors.fill: parent
SubscriptionExpiredDrawer {
id: subscriptionExpiredDrawer
anchors.fill: parent
}
}
Connections {
target: ApiConfigsController
function onSubscriptionExpiredOnServer() {
subscriptionExpiredDrawer.openTriggered()
}
}
Connections {
target: ApiSettingsController
function onRenewalLinkReceived(url) {
Qt.openUrlExternally(url)
}
}
Item {
objectName: "busyIndicatorItem"

View File

@@ -39,9 +39,8 @@ VpnConnection::VpnConnection(std::shared_ptr<Settings> settings, QObject *parent
{
#if defined(Q_OS_IOS) || defined(MACOS_NE)
m_checkTimer.setInterval(1000);
connect(IosController::Instance(), &IosController::connectionStateChanged, this, &VpnConnection::onConnectionStateChanged);
connect(IosController::Instance(), &IosController::connectionStateChanged, this, &VpnConnection::setConnectionState);
connect(IosController::Instance(), &IosController::bytesChanged, this, &VpnConnection::onBytesChanged);
#endif
}
@@ -59,7 +58,7 @@ void VpnConnection::onKillSwitchModeChanged(bool enabled)
#ifdef AMNEZIA_DESKTOP
IpcClient::withInterface([enabled](QSharedPointer<IpcInterfaceReplica> iface){
QRemoteObjectPendingReply<bool> reply = iface->refreshKillSwitch(enabled);
if (reply.waitForFinished(1000) && reply.returnValue())
if (reply.waitForFinished() && reply.returnValue())
qDebug() << "VpnConnection::onKillSwitchModeChanged: Killswitch refreshed";
else
qWarning() << "VpnConnection::onKillSwitchModeChanged: Failed to execute remote refreshKillSwitch call";
@@ -73,40 +72,57 @@ void VpnConnection::onConnectionStateChanged(Vpn::ConnectionState state)
auto container = m_settings->defaultContainer(m_settings->defaultServerIndex());
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
if (state == Vpn::ConnectionState::Connected) {
iface->resetIpStack();
iface->flushDns();
switch (state) {
case Vpn::ConnectionState::Connected: {
iface->resetIpStack();
if (!ContainerProps::isAwgContainer(container) &&
container != DockerContainer::WireGuard) {
QString dns1 = m_vpnConfiguration.value(config_key::dns1).toString();
QString dns2 = m_vpnConfiguration.value(config_key::dns2).toString();
auto flushDns = iface->flushDns();
if (flushDns.waitForFinished() && flushDns.returnValue())
qDebug() << "VpnConnection::onConnectionStateChanged: Successfully flushed DNS";
else
qWarning() << "VpnConnection::onConnectionStateChanged: Failed to clear saved routes";
iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << dns1 << dns2);
if (m_settings->isSitesSplitTunnelingEnabled()) {
iface->routeDeleteList(m_vpnProtocol->vpnGateway(), QStringList() << "0.0.0.0");
// qDebug() << "VpnConnection::onConnectionStateChanged :: adding custom routes, count:" << forwardIps.size();
if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) {
QTimer::singleShot(1000, m_vpnProtocol.data(),
[this]() { addSitesRoutes(m_vpnProtocol->vpnGateway(), m_settings->routeMode()); });
} else if (m_settings->routeMode() == Settings::VpnAllExceptSites) {
iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << "0.0.0.0/1");
iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << "128.0.0.0/1");
if (!ContainerProps::isAwgContainer(container) &&
container != DockerContainer::WireGuard) {
QString dns1 = m_vpnConfiguration.value(config_key::dns1).toString();
QString dns2 = m_vpnConfiguration.value(config_key::dns2).toString();
iface->routeAddList(m_vpnProtocol->routeGateway(), QStringList() << remoteAddress());
addSitesRoutes(m_vpnProtocol->routeGateway(), m_settings->routeMode());
// TODO: add error code handling for all routeAddList (or rework the code below)
iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << dns1 << dns2);
if (m_settings->isSitesSplitTunnelingEnabled()) {
iface->routeDeleteList(m_vpnProtocol->vpnGateway(), QStringList() << "0.0.0.0");
// qDebug() << "VpnConnection::onConnectionStateChanged :: adding custom routes, count:" << forwardIps.size();
if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) {
QTimer::singleShot(1000, m_vpnProtocol.data(),
[this]() { addSitesRoutes(m_vpnProtocol->vpnGateway(), m_settings->routeMode()); });
} else if (m_settings->routeMode() == Settings::VpnAllExceptSites) {
iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << "0.0.0.0/1");
iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << "128.0.0.0/1");
iface->routeAddList(m_vpnProtocol->routeGateway(), QStringList() << remoteAddress());
addSitesRoutes(m_vpnProtocol->routeGateway(), m_settings->routeMode());
}
}
}
}
} else if (state == Vpn::ConnectionState::Error) {
iface->flushDns();
} break;
case Vpn::ConnectionState::Disconnected:
case Vpn::ConnectionState::Error: {
auto flushDns = iface->flushDns();
if (flushDns.waitForFinished() && flushDns.returnValue())
qDebug() << "VpnConnection::onConnectionStateChanged: Successfully flushed DNS";
else
qWarning() << "VpnConnection::onConnectionStateChanged: Failed to flush DNS";
if (m_settings->isSitesSplitTunnelingEnabled()) {
if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) {
iface->clearSavedRoutes();
}
}
auto clearSavedRoutes = iface->clearSavedRoutes();
if (clearSavedRoutes.waitForFinished() && clearSavedRoutes.returnValue())
qDebug() << "VpnConnection::onConnectionStateChanged: Successfully cleared saved routes";
else
qWarning() << "VpnConnection::onConnectionStateChanged: Failed to clear saved routes";
} break;
default:
break;
}
});
#endif
@@ -120,7 +136,6 @@ void VpnConnection::onConnectionStateChanged(Vpn::ConnectionState state)
m_checkTimer.stop();
}
#endif
emit connectionStateChanged(state);
}
const QString &VpnConnection::remoteAddress() const
@@ -165,7 +180,11 @@ void VpnConnection::addSitesRoutes(const QString &gw, Settings::RouteMode mode)
});
m_settings->addVpnSite(mode, site, ip);
}
flushDns();
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
auto reply = iface->flushDns();
if (reply.waitForFinished() || !reply.returnValue())
qWarning() << "VpnConnection::addSitesRoutes: Failed to flush DNS";
});
break;
}
}
@@ -180,48 +199,6 @@ QSharedPointer<VpnProtocol> VpnConnection::vpnProtocol() const
return m_vpnProtocol;
}
void VpnConnection::addRoutes(const QStringList &ips)
{
#ifdef AMNEZIA_DESKTOP
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
if (connectionState() == Vpn::ConnectionState::Connected) {
if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) {
iface->routeAddList(m_vpnProtocol->vpnGateway(), ips);
} else if (m_settings->routeMode() == Settings::VpnAllExceptSites) {
iface->routeAddList(m_vpnProtocol->routeGateway(), ips);
}
}
});
#endif
}
void VpnConnection::deleteRoutes(const QStringList &ips)
{
#ifdef AMNEZIA_DESKTOP
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
if (connectionState() == Vpn::ConnectionState::Connected) {
if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) {
iface->routeDeleteList(vpnProtocol()->vpnGateway(), ips);
} else if (m_settings->routeMode() == Settings::VpnAllExceptSites) {
iface->routeDeleteList(m_vpnProtocol->routeGateway(), ips);
}
}
});
#endif
}
void VpnConnection::flushDns()
{
#ifdef AMNEZIA_DESKTOP
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
auto reply = iface->flushDns();
if (reply.waitForFinished(1000) || !reply.returnValue()) {
qWarning() << "VpnConnection::flushDns(): Failed to flush DNS";
}
});
#endif
}
void VpnConnection::disconnectSlots()
{
if (m_vpnProtocol) {
@@ -251,7 +228,7 @@ void VpnConnection::connectToVpn(int serverIndex, const ServerCredentials &crede
<< m_settings->routeMode();
m_remoteAddress = NetworkUtilities::getIPAddress(credentials.hostName);
emit connectionStateChanged(Vpn::ConnectionState::Connecting);
setConnectionState(Vpn::ConnectionState::Connecting);
m_vpnConfiguration = vpnConfiguration;
@@ -269,7 +246,7 @@ void VpnConnection::connectToVpn(int serverIndex, const ServerCredentials &crede
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
m_vpnProtocol.reset(VpnProtocol::factory(container, m_vpnConfiguration));
if (!m_vpnProtocol) {
emit connectionStateChanged(Vpn::ConnectionState::Error);
setConnectionState(Vpn::ConnectionState::Error);
return;
}
m_vpnProtocol->prepare();
@@ -287,17 +264,24 @@ void VpnConnection::connectToVpn(int serverIndex, const ServerCredentials &crede
createProtocolConnections();
ErrorCode errorCode = m_vpnProtocol->start();
if (errorCode != ErrorCode::NoError)
emit connectionStateChanged(Vpn::ConnectionState::Error);
if (ErrorCode err = m_vpnProtocol->start(); err != ErrorCode::NoError) {
setConnectionState(Vpn::ConnectionState::Error);
emit vpnProtocolError(err);
}
}
void VpnConnection::createProtocolConnections()
{
connect(m_vpnProtocol.data(), &VpnProtocol::protocolError, this, &VpnConnection::vpnProtocolError);
connect(m_vpnProtocol.data(), SIGNAL(connectionStateChanged(Vpn::ConnectionState)), this,
SLOT(onConnectionStateChanged(Vpn::ConnectionState)));
connect(m_vpnProtocol.data(), &VpnProtocol::connectionStateChanged, this, &VpnConnection::setConnectionState);
connect(m_vpnProtocol.data(), SIGNAL(bytesChanged(quint64, quint64)), this, SLOT(onBytesChanged(quint64, quint64)));
#ifdef AMNEZIA_DESKTOP
IpcClient::withInterface([this](QSharedPointer<IpcInterfaceReplica> rep) {
connect(rep.data(), &IpcInterfaceReplica::networkChanged, this, &VpnConnection::reconnectToVpn, Qt::QueuedConnection);
connect(rep.data(), &IpcInterfaceReplica::wakeup, this, &VpnConnection::reconnectToVpn, Qt::QueuedConnection);
});
#endif
}
void VpnConnection::appendKillSwitchConfig()
@@ -439,6 +423,27 @@ QString VpnConnection::bytesPerSecToText(quint64 bytes)
return QString("%1 %2").arg(QString::number(mbps, 'f', 2)).arg(tr("Mbps")); // Mbit/s
}
void VpnConnection::reconnectToVpn() {
if (m_vpnProtocol.isNull())
return;
if (m_connectionState != Vpn::ConnectionState::Connected) {
qWarning() << QString("Reconnect triggered on %1 during inappropriate state: %2; ignoring slot")
.arg(QMetaEnum::fromType<Vpn::ConnectionState>().valueToKey(m_connectionState));
return;
}
qDebug() << "Reconnect triggered. Reconnecting to the server";
setConnectionState(Vpn::ConnectionState::Reconnecting);
m_vpnProtocol->stop();
if (ErrorCode err = m_vpnProtocol->start(); err != ErrorCode::NoError) {
setConnectionState(Vpn::ConnectionState::Error);
emit vpnProtocolError(err);
}
}
void VpnConnection::disconnectFromVpn()
{
#if defined(Q_OS_IOS) || defined(MACOS_NE)
@@ -448,41 +453,26 @@ void VpnConnection::disconnectFromVpn()
#endif
if (m_vpnProtocol.isNull()) {
emit connectionStateChanged(Vpn::ConnectionState::Disconnected);
setConnectionState(Vpn::ConnectionState::Disconnected);
return;
}
m_vpnProtocol->stop();
#ifdef AMNEZIA_DESKTOP
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
QRemoteObjectPendingReply<bool> flushReply = iface->flushDns();
if (flushReply.waitForFinished(5000) && flushReply.returnValue())
qDebug() << "VpnConnection::disconnectFromVpn(): Successfully flushed DNS";
else
qWarning() << "VpnConnection::disconnectFromVpn(): Failed to flush DNS";
QRemoteObjectPendingReply<bool> clearSavedRoutesReply = iface->clearSavedRoutes();
if (clearSavedRoutesReply.waitForFinished(5000) && clearSavedRoutesReply.returnValue())
qDebug() << "VpnConnection::disconnectFromVpn(): Successfully cleared saved routes";
else
qWarning() << "VpnConnection::disconnectFromVpn(): Failed to clear saved routes";
});
#endif
setConnectionState(Vpn::ConnectionState::Disconnecting);
#ifdef Q_OS_ANDROID
auto *const connection = new QMetaObject::Connection;
*connection = connect(AndroidController::instance(), &AndroidController::vpnStateChanged, this,
[this, connection](AndroidController::ConnectionState state) {
if (state == AndroidController::ConnectionState::DISCONNECTED) {
onConnectionStateChanged(Vpn::ConnectionState::Disconnected);
setConnectionState(Vpn::ConnectionState::Disconnected);
disconnect(*connection);
delete connection;
}
});
m_vpnProtocol->stop();
#endif
m_vpnProtocol->stop();
#if !defined(Q_OS_ANDROID) && !defined(AMNEZIA_DESKTOP)
m_vpnProtocol->deleteLater();
#endif
@@ -490,27 +480,12 @@ void VpnConnection::disconnectFromVpn()
m_vpnProtocol = nullptr;
}
Vpn::ConnectionState VpnConnection::connectionState()
{
if (!m_vpnProtocol)
return Vpn::ConnectionState::Disconnected;
return m_vpnProtocol->connectionState();
}
bool VpnConnection::isConnected() const
{
if (m_vpnProtocol.isNull()) {
return false;
}
return m_vpnProtocol->isConnected();
}
bool VpnConnection::isDisconnected() const
{
if (m_vpnProtocol.isNull()) {
return true;
}
return m_vpnProtocol->isDisconnected();
void VpnConnection::setConnectionState(Vpn::ConnectionState state) {
onConnectionStateChanged(state);
if (state == Vpn::Disconnected && m_connectionState == Vpn::Reconnecting)
return;
m_connectionState = state;
emit connectionStateChanged(state);
}

View File

@@ -34,10 +34,6 @@ public:
ErrorCode lastError() const;
bool isConnected() const;
bool isDisconnected() const;
Vpn::ConnectionState connectionState();
QSharedPointer<VpnProtocol> vpnProtocol() const;
const QString &remoteAddress() const;
@@ -48,14 +44,10 @@ public:
#endif
public slots:
void connectToVpn(int serverIndex,
const ServerCredentials &credentials, DockerContainer container, const QJsonObject &vpnConfiguration);
void connectToVpn(int serverIndex, const ServerCredentials &credentials, DockerContainer container, const QJsonObject &vpnConfiguration);
void reconnectToVpn();
void disconnectFromVpn();
void addRoutes(const QStringList &ips);
void deleteRoutes(const QStringList &ips);
void flushDns();
void onKillSwitchModeChanged(bool enabled);
void disconnectSlots();
@@ -70,6 +62,8 @@ protected slots:
void onBytesChanged(quint64 receivedBytes, quint64 sentBytes);
void onConnectionStateChanged(Vpn::ConnectionState state);
void setConnectionState(Vpn::ConnectionState state);
protected:
QSharedPointer<VpnProtocol> m_vpnProtocol;
@@ -89,6 +83,8 @@ private:
void createAndroidConnections();
#endif
Vpn::ConnectionState m_connectionState;
void createProtocolConnections();
void appendSplitTunnelingConfig();

View File

@@ -45,5 +45,6 @@ class IpcInterface
SLOT( bool stopNetworkCheck() );
SIGNAL( connectionLose() );
SIGNAL( networkChange() );
SIGNAL( wakeup() );
SIGNAL( networkChanged() );
};

View File

@@ -33,18 +33,10 @@ KillSwitch* KillSwitch::instance()
bool KillSwitch::init()
{
#ifdef Q_OS_LINUX
if (!LinuxFirewall::isInstalled()) {
LinuxFirewall::install();
}
m_appSettigns = QSharedPointer<SecureQSettings>(new SecureQSettings(ORGANIZATION_NAME, APPLICATION_NAME, nullptr));
#endif
#ifdef Q_OS_MACOS
if (!MacOSFirewall::isInstalled()) {
MacOSFirewall::install();
}
#if defined(Q_OS_LINUX) || defined(Q_OS_MACOS)
m_appSettigns = QSharedPointer<SecureQSettings>(new SecureQSettings(ORGANIZATION_NAME, APPLICATION_NAME, nullptr));
#endif
if (isStrictKillSwitchEnabled()) {
return disableAllTraffic();
}
@@ -79,7 +71,6 @@ bool KillSwitch::isStrictKillSwitchEnabled()
+ "\\" + QString(APPLICATION_NAME), QSettings::NativeFormat);
return RegHLM.value("strictKillSwitchEnabled", false).toBool();
#endif
m_appSettigns->sync();
return m_appSettigns->value("Conf/strictKillSwitchEnabled", false).toBool();
}

View File

@@ -50,8 +50,8 @@ LocalServer::LocalServer(QObject *parent) : QObject(parent),
}
m_networkWatcher.initialize();
connect(&m_networkWatcher, &NetworkWatcher::sleepMode, &m_ipcServer, &IpcServer::networkChange);
connect(&m_networkWatcher, &NetworkWatcher::networkChange, &m_ipcServer, &IpcServer::networkChange);
connect(&m_networkWatcher, &NetworkWatcher::networkChanged, &m_ipcServer, &IpcServer::networkChanged);
connect(&m_networkWatcher, &NetworkWatcher::wakeup, &m_ipcServer, &IpcServer::wakeup);
KillSwitch::instance()->init();
#ifdef Q_OS_LINUX