Compare commits

...

18 Commits

Author SHA1 Message Date
lunardunno
23eff43575 Open docs url according for Ukrainian language 2024-05-24 11:53:09 +04:00
lunardunno
92d9071b79 Open docs url according for Ukrainian language 2024-05-24 11:35:34 +04:00
Vladyslav Miachkov
8cbc5c6a88 Fix wrong identation 2024-05-20 10:12:54 +03:00
Vladyslav Miachkov
1ada5e7ab9 Refactor error message logic 2024-05-19 14:50:39 +03:00
Vladyslav Miachkov
f7edba7757 Open docs url according to language set 2024-05-19 14:15:23 +03:00
Vladyslav Miachkov
e672a4a471 Change mouse icon on link hover 2024-05-19 14:15:23 +03:00
Vladyslav Miachkov
1bc43a9c06 Add clickable docs url on error 2024-05-19 14:15:23 +03:00
pokamest
53fdf5f70d Merge pull request #811 from amnezia-vpn/feature/api-payload-info
change pretty product name to product type for api payload
2024-05-17 04:57:00 -07:00
vladimir.kuznetsov
871aced1d1 change pretty product name to product type for api payload 2024-05-17 09:40:02 +02:00
Nethius
2254bfc128 added the OS version and application version to the api request payload (#810)
* added the OS version and application version to the api request payload
* added errorStrings for new api error codes
2024-05-16 18:57:51 +01:00
pokamest
b71dcb8dd0 Merge pull request #808 from theLastOfCats/dev
Remove misleading iOS and Android support from IPSec protocol transtation strings
2024-05-16 06:22:43 -07:00
pokamest
33d1518fd2 Request internet permission before connect for iOS (#794)
* Attempt to fix API error 1100
* NSURLSession fake call to exec iOS network settings dialog
* use http://captive.apple.com/generate_204 for requesting internet
permission
* moved MobileUtils to IosController
* replaced callbacks with signal-slots in apiController
2024-05-16 14:19:56 +01:00
Shagit Ziganshin
ee5344a4ea Remove misleading iOS and Android support from IPSec protocol transtation strings.
Signed-off-by: Shagit Ziganshin <3687591+theLastOfCats@users.noreply.github.com>
2024-05-14 01:14:05 +03:00
albexk
abb3c918e3 Android notification and routing (#797)
Android notification and routing
2024-05-12 16:04:14 +01:00
Vladyslav Miachkov
ff348a348c Add checking background service before connect (#716)
checking if the service is running for all platforms
2024-05-10 11:06:04 +01:00
pokamest
d67c378bff Merge pull request #800 from amnezia-vpn/bugfix/ssh-check-connection
pass errorCode by reference in configurators
2024-05-10 03:03:59 -07:00
vladimir.kuznetsov
d85a0439c5 pass errorCode by reference in configurators 2024-05-09 20:56:52 +03:00
Mykola Baibuz
5bd8c33a6d Update Mozilla upstream (#790)
* Update Mozilla upstream
2024-05-08 22:02:02 +01:00
123 changed files with 1965 additions and 1120 deletions

View File

@@ -127,7 +127,6 @@ set(HEADERS ${HEADERS}
${CMAKE_CURRENT_LIST_DIR}/core/controllers/vpnConfigurationController.h
${CMAKE_CURRENT_LIST_DIR}/protocols/protocols_defs.h
${CMAKE_CURRENT_LIST_DIR}/protocols/qml_register_protocols.h
${CMAKE_CURRENT_LIST_DIR}/ui/notificationhandler.h
${CMAKE_CURRENT_LIST_DIR}/ui/pages.h
${CMAKE_CURRENT_LIST_DIR}/ui/property_helper.h
${CMAKE_CURRENT_LIST_DIR}/ui/qautostart.h
@@ -152,11 +151,16 @@ include_directories(mozilla/models)
if(NOT IOS)
set(HEADERS ${HEADERS}
${CMAKE_CURRENT_LIST_DIR}/platforms/ios/MobileUtils.h
${CMAKE_CURRENT_LIST_DIR}/platforms/ios/QRCodeReaderBase.h
)
endif()
if(NOT ANDROID)
set(HEADERS ${HEADERS}
${CMAKE_CURRENT_LIST_DIR}/ui/notificationhandler.h
)
endif()
set(SOURCES ${SOURCES}
${CMAKE_CURRENT_LIST_DIR}/migrations.cpp
${CMAKE_CURRENT_LIST_DIR}/amnezia_application.cpp
@@ -168,7 +172,6 @@ set(SOURCES ${SOURCES}
${CMAKE_CURRENT_LIST_DIR}/core/controllers/serverController.cpp
${CMAKE_CURRENT_LIST_DIR}/core/controllers/vpnConfigurationController.cpp
${CMAKE_CURRENT_LIST_DIR}/protocols/protocols_defs.cpp
${CMAKE_CURRENT_LIST_DIR}/ui/notificationhandler.cpp
${CMAKE_CURRENT_LIST_DIR}/ui/qautostart.cpp
${CMAKE_CURRENT_LIST_DIR}/protocols/vpnprotocol.cpp
${CMAKE_CURRENT_LIST_DIR}/core/sshclient.cpp
@@ -189,11 +192,16 @@ endif()
if(NOT IOS)
set(SOURCES ${SOURCES}
${CMAKE_CURRENT_LIST_DIR}/platforms/ios/MobileUtils.cpp
${CMAKE_CURRENT_LIST_DIR}/platforms/ios/QRCodeReaderBase.cpp
)
endif()
if(NOT ANDROID)
set(SOURCES ${SOURCES}
${CMAKE_CURRENT_LIST_DIR}/ui/notificationhandler.cpp
)
endif()
file(GLOB COMMON_FILES_H CONFIGURE_DEPENDS ${CMAKE_CURRENT_LIST_DIR}/*.h)
file(GLOB COMMON_FILES_CPP CONFIGURE_DEPENDS ${CMAKE_CURRENT_LIST_DIR}/*.cpp)

View File

@@ -55,6 +55,7 @@ AmneziaApplication::AmneziaApplication(int &argc, char *argv[], bool allowSecond
#endif
m_settings = std::shared_ptr<Settings>(new Settings);
m_nam = new QNetworkAccessManager(this);
}
AmneziaApplication::~AmneziaApplication()
@@ -142,6 +143,7 @@ void AmneziaApplication::init()
connect(m_settings.get(), &Settings::screenshotsEnabledChanged, [](bool enabled) { AmneziaVPN::toggleScreenshots(enabled); });
#endif
#ifndef Q_OS_ANDROID
m_notificationHandler.reset(NotificationHandler::create(nullptr));
connect(m_vpnConnection.get(), &VpnConnection::connectionStateChanged, m_notificationHandler.get(),
@@ -149,10 +151,11 @@ void AmneziaApplication::init()
connect(m_notificationHandler.get(), &NotificationHandler::raiseRequested, m_pageController.get(), &PageController::raiseMainWindow);
connect(m_notificationHandler.get(), &NotificationHandler::connectRequested, m_connectionController.get(),
&ConnectionController::openConnection);
static_cast<void (ConnectionController::*)()>(&ConnectionController::openConnection));
connect(m_notificationHandler.get(), &NotificationHandler::disconnectRequested, m_connectionController.get(),
&ConnectionController::closeConnection);
connect(this, &AmneziaApplication::translationsUpdated, m_notificationHandler.get(), &NotificationHandler::onTranslationsUpdated);
#endif
m_engine->load(url);
m_systemController->setQmlRoot(m_engine->rootObjects().value(0));
@@ -360,16 +363,22 @@ void AmneziaApplication::initControllers()
new ConnectionController(m_serversModel, m_containersModel, m_clientManagementModel, m_vpnConnection, m_settings));
m_engine->rootContext()->setContextProperty("ConnectionController", m_connectionController.get());
connect(m_connectionController.get(), &ConnectionController::connectionErrorOccurred, this, [this](const QString &errorMessage) {
connect(m_connectionController.get(), qOverload<const QString &>(&ConnectionController::connectionErrorOccurred), this, [this](const QString &errorMessage) {
emit m_pageController->showErrorMessage(errorMessage);
emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected);
});
connect(m_connectionController.get(), qOverload<ErrorCode>(&ConnectionController::connectionErrorOccurred), this, [this](ErrorCode errorCode) {
emit m_pageController->showErrorMessage(errorCode);
emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected);
});
connect(m_connectionController.get(), &ConnectionController::connectButtonClicked, m_connectionController.get(),
&ConnectionController::toggleConnection, Qt::QueuedConnection);
connect(this, &AmneziaApplication::translationsUpdated, m_connectionController.get(), &ConnectionController::onTranslationsUpdated);
m_pageController.reset(new PageController(m_serversModel, m_settings));
m_pageController.reset(new PageController(m_serversModel, m_settings, m_languageModel));
m_engine->rootContext()->setContextProperty("PageController", m_pageController.get());
m_installController.reset(new InstallController(m_serversModel, m_containersModel, m_protocolsModel, m_clientManagementModel, m_settings));

View File

@@ -2,6 +2,7 @@
#define AMNEZIA_APPLICATION_H
#include <QCommandLineParser>
#include <QNetworkAccessManager>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QThread>
@@ -26,7 +27,9 @@
#include "ui/models/containers_model.h"
#include "ui/models/languageModel.h"
#include "ui/models/protocols/cloakConfigModel.h"
#include "ui/notificationhandler.h"
#ifndef Q_OS_ANDROID
#include "ui/notificationhandler.h"
#endif
#ifdef Q_OS_WINDOWS
#include "ui/models/protocols/ikev2ConfigModel.h"
#endif
@@ -73,6 +76,7 @@ public:
bool parseCommands();
QQmlApplicationEngine *qmlEngine() const;
QNetworkAccessManager *manager() { return m_nam; }
signals:
void translationsUpdated();
@@ -113,7 +117,9 @@ private:
QSharedPointer<VpnConnection> m_vpnConnection;
QThread m_vpnConnectionThread;
#ifndef Q_OS_ANDROID
QScopedPointer<NotificationHandler> m_notificationHandler;
#endif
QScopedPointer<ConnectionController> m_connectionController;
QScopedPointer<PageController> m_pageController;
@@ -124,6 +130,8 @@ private:
QScopedPointer<SitesController> m_sitesController;
QScopedPointer<SystemController> m_systemController;
QScopedPointer<AppSplitTunnelingController> m_appSplitTunnelingController;
QNetworkAccessManager *m_nam;
};
#endif // AMNEZIA_APPLICATION_H

View File

@@ -3,9 +3,6 @@ package org.amnezia.vpn.protocol.cloak
import android.util.Base64
import net.openvpn.ovpn3.ClientAPI_Config
import org.amnezia.vpn.protocol.openvpn.OpenVpn
import org.amnezia.vpn.protocol.openvpn.OpenVpnConfig
import org.amnezia.vpn.util.net.InetNetwork
import org.amnezia.vpn.util.net.parseInetAddress
import org.json.JSONObject
/**
@@ -54,13 +51,6 @@ class Cloak : OpenVpn() {
return openVpnConfig
}
override fun configPluggableTransport(configBuilder: OpenVpnConfig.Builder, config: JSONObject) {
// exclude remote server ip from vpn routes
val remoteServer = config.getString("hostName")
val remoteServerAddress = InetNetwork(parseInetAddress(remoteServer))
configBuilder.excludeRoute(remoteServerAddress)
}
private fun checkCloakJson(cloakConfigJson: JSONObject): JSONObject {
cloakConfigJson.put("NumConn", 1)
cloakConfigJson.put("ProxyMethod", "openvpn")

View File

@@ -13,7 +13,9 @@ import org.amnezia.vpn.protocol.ProtocolState
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
import org.amnezia.vpn.protocol.Statistics
import org.amnezia.vpn.protocol.VpnStartException
import org.amnezia.vpn.util.net.InetNetwork
import org.amnezia.vpn.util.net.getLocalNetworks
import org.amnezia.vpn.util.net.parseInetAddress
import org.json.JSONObject
/**
@@ -77,6 +79,12 @@ open class OpenVpn : Protocol() {
if (evalConfig.error) {
throw BadConfigException("OpenVPN config parse error: ${evalConfig.message}")
}
// exclude remote server ip from vpn routes
val remoteServer = config.getString("hostName")
val remoteServerAddress = InetNetwork(parseInetAddress(remoteServer))
configBuilder.excludeRoute(remoteServerAddress)
configPluggableTransport(configBuilder, config)
configBuilder.configSplitTunneling(config)
configBuilder.configAppSplitTunneling(config)

View File

@@ -105,15 +105,17 @@ abstract class Protocol {
vpnBuilder.addSearchDomain(it)
}
for (addr in config.routes) {
Log.d(TAG, "addRoute: $addr")
vpnBuilder.addRoute(addr)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
for (addr in config.excludedRoutes) {
Log.d(TAG, "excludeRoute: $addr")
vpnBuilder.excludeRoute(addr)
for ((inetNetwork, include) in config.routes) {
if (include) {
Log.d(TAG, "addRoute: $inetNetwork")
vpnBuilder.addRoute(inetNetwork)
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Log.d(TAG, "excludeRoute: $inetNetwork")
vpnBuilder.excludeRoute(inetNetwork)
} else {
Log.e(TAG, "Trying to exclude route $inetNetwork on old Android")
}
}
}

View File

@@ -12,8 +12,7 @@ open class ProtocolConfig protected constructor(
val addresses: Set<InetNetwork>,
val dnsServers: Set<InetAddress>,
val searchDomain: String?,
val routes: Set<InetNetwork>,
val excludedRoutes: Set<InetNetwork>,
val routes: Set<Route>,
val includedAddresses: Set<InetNetwork>,
val excludedAddresses: Set<InetNetwork>,
val includedApplications: Set<String>,
@@ -29,7 +28,6 @@ open class ProtocolConfig protected constructor(
builder.dnsServers,
builder.searchDomain,
builder.routes,
builder.excludedRoutes,
builder.includedAddresses,
builder.excludedAddresses,
builder.includedApplications,
@@ -43,8 +41,7 @@ open class ProtocolConfig protected constructor(
open class Builder(blockingMode: Boolean) {
internal val addresses: MutableSet<InetNetwork> = hashSetOf()
internal val dnsServers: MutableSet<InetAddress> = hashSetOf()
internal val routes: MutableSet<InetNetwork> = hashSetOf()
internal val excludedRoutes: MutableSet<InetNetwork> = hashSetOf()
internal val routes: MutableSet<Route> = mutableSetOf()
internal val includedAddresses: MutableSet<InetNetwork> = hashSetOf()
internal val excludedAddresses: MutableSet<InetNetwork> = hashSetOf()
internal val includedApplications: MutableSet<String> = hashSetOf()
@@ -77,13 +74,21 @@ open class ProtocolConfig protected constructor(
fun setSearchDomain(domain: String) = apply { this.searchDomain = domain }
fun addRoute(route: InetNetwork) = apply { this.routes += route }
fun addRoutes(routes: Collection<InetNetwork>) = apply { this.routes += routes }
fun removeRoute(route: InetNetwork) = apply { this.routes.remove(route) }
fun addRoute(route: InetNetwork) = apply { this.routes += Route(route, true) }
fun addRoutes(routes: Collection<InetNetwork>) = apply { this.routes += routes.map { Route(it, true) } }
fun excludeRoute(route: InetNetwork) = apply { this.routes += Route(route, false) }
fun excludeRoutes(routes: Collection<InetNetwork>) = apply { this.routes += routes.map { Route(it, false) } }
fun removeRoute(route: InetNetwork) = apply { this.routes.removeIf { it.inetNetwork == route } }
fun clearRoutes() = apply { this.routes.clear() }
fun excludeRoute(route: InetNetwork) = apply { this.excludedRoutes += route }
fun excludeRoutes(routes: Collection<InetNetwork>) = apply { this.excludedRoutes += routes }
fun prependRoutes(block: Builder.() -> Unit) = apply {
val savedRoutes = mutableListOf<Route>().apply { addAll(routes) }
routes.clear()
block()
routes.addAll(savedRoutes)
}
fun includeAddress(addr: InetNetwork) = apply { this.includedAddresses += addr }
fun includeAddresses(addresses: Collection<InetNetwork>) = apply { this.includedAddresses += addresses }
@@ -117,37 +122,46 @@ open class ProtocolConfig protected constructor(
// remove default routes, if any
removeRoute(InetNetwork("0.0.0.0", 0))
removeRoute(InetNetwork("::", 0))
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
// for older versions of Android, add the default route to the excluded routes
// to correctly build the excluded subnets list later
excludeRoute(InetNetwork("0.0.0.0", 0))
removeRoute(InetNetwork("2000::", 3))
prependRoutes {
addRoutes(includedAddresses)
}
addRoutes(includedAddresses)
} else if (excludedAddresses.isNotEmpty()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// default routes are required for split tunneling in newer versions of Android
prependRoutes {
addRoute(InetNetwork("0.0.0.0", 0))
addRoute(InetNetwork("::", 0))
addRoute(InetNetwork("2000::", 3))
excludeRoutes(excludedAddresses)
}
excludeRoutes(excludedAddresses)
}
}
private fun processExcludedRoutes() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU && excludedRoutes.isNotEmpty()) {
// todo: rewrite, taking into account the current routes
// for older versions of Android, build a list of subnets without excluded routes
// and add them to routes
val ipRangeSet = IpRangeSet()
ipRangeSet.remove(IpRange("127.0.0.0", 8))
excludedRoutes.forEach {
ipRangeSet.remove(IpRange(it))
private fun processRoutes() {
// replace ::/0 as it may cause LAN connection issues
val ipv6DefaultRoute = InetNetwork("::", 0)
if (routes.removeIf { it.include && it.inetNetwork == ipv6DefaultRoute }) {
prependRoutes {
addRoute(InetNetwork("2000::", 3))
}
// remove default routes, if any
removeRoute(InetNetwork("0.0.0.0", 0))
removeRoute(InetNetwork("::", 0))
}
// for older versions of Android, build a list of subnets without excluded routes
// and add them to routes
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU && routes.any { !it.include }) {
val ipRangeSet = IpRangeSet()
routes.forEach {
if (it.include) ipRangeSet.add(IpRange(it.inetNetwork))
else ipRangeSet.remove(IpRange(it.inetNetwork))
}
ipRangeSet.remove(IpRange("127.0.0.0", 8))
ipRangeSet.remove(IpRange("::1", 128))
routes.clear()
ipRangeSet.subnets().forEach(::addRoute)
addRoute(InetNetwork("2000::", 3))
}
// filter ipv4 and ipv6 loopback addresses
val ipv6Loopback = InetNetwork("::1", 128)
routes.removeIf {
it.include &&
if (it.inetNetwork.isIpv4) it.inetNetwork.address.address[0] == 127.toByte()
else it.inetNetwork == ipv6Loopback
}
}
@@ -165,7 +179,7 @@ open class ProtocolConfig protected constructor(
protected fun configBuild() {
processSplitTunneling()
processExcludedRoutes()
processRoutes()
validate()
}
@@ -177,3 +191,5 @@ open class ProtocolConfig protected constructor(
Builder(blockingMode).apply(block).build()
}
}
data class Route(val inetNetwork: InetNetwork, val include: Boolean)

View File

@@ -1,12 +1,26 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="connecting">Подключение</string>
<string name="disconnecting">Отключение</string>
<string name="cancel">Отмена</string>
<string name="disconnected">Не подключено</string>
<string name="connected">Подключено</string>
<string name="connecting">Подключение…</string>
<string name="disconnecting">Отключение…</string>
<string name="reconnecting">Переподключение…</string>
<string name="connect">Подключиться</string>
<string name="disconnect">Отключиться</string>
<string name="ok">ОК</string>
<string name="cancel">Отмена</string>
<string name="yes">Да</string>
<string name="no">Нет</string>
<string name="vpnGranted">VPN-подключение разрешено</string>
<string name="vpnDenied">VPN-подключение запрещено</string>
<string name="vpnSetupFailed">Ошибка настройки VPN</string>
<string name="vpnSetupFailedMessage">Чтобы подключиться к AmneziaVPN необходимо:\n\n- Разрешить приложению подключаться к сети VPN\n- Отключить функцию \"Постоянная VPN\" для всех остальных VPN-приложений в системных настройках VPN</string>
<string name="openVpnSettings">Открыть настройки VPN</string>
<string name="notificationChannelDescription">Уведомления сервиса AmneziaVPN</string>
<string name="notificationDialogTitle">Сервис AmneziaVPN</string>
<string name="notificationDialogMessage">Показывать статус VPN в строке состояния?</string>
<string name="notificationSettingsDialogTitle">Настройки уведомлений</string>
<string name="notificationSettingsDialogMessage">Для показа уведомлений необходимо включить уведомления в системных настройках</string>
<string name="openNotificationSettings">Открыть настройки уведомлений</string>
</resources>

View File

@@ -1,12 +1,26 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="connecting">Connecting</string>
<string name="disconnecting">Disconnecting</string>
<string name="cancel">Cancel</string>
<string name="disconnected">Not connected</string>
<string name="connected">Connected</string>
<string name="connecting">Connecting…</string>
<string name="disconnecting">Disconnecting…</string>
<string name="reconnecting">Reconnecting…</string>
<string name="connect">Connect</string>
<string name="disconnect">Disconnect</string>
<string name="ok">OK</string>
<string name="cancel">Cancel</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="vpnGranted">VPN permission granted</string>
<string name="vpnDenied">VPN permission denied</string>
<string name="vpnSetupFailed">VPN setup error</string>
<string name="vpnSetupFailedMessage">To connect to AmneziaVPN, please do the following:\n\n- Allow the app to set up a VPN connection\n- Disable Always-on VPN for any other VPN app in the VPN system settings</string>
<string name="openVpnSettings">Open VPN settings</string>
<string name="notificationChannelDescription">AmneziaVPN service notification</string>
<string name="notificationDialogTitle">AmneziaVPN service</string>
<string name="notificationDialogMessage">Show the VPN state in the status bar?</string>
<string name="notificationSettingsDialogTitle">Notification settings</string>
<string name="notificationSettingsDialogMessage">To show notifications, you must enable notifications in the system settings</string>
<string name="openNotificationSettings">Open notification settings</string>
</resources>

View File

@@ -1,6 +1,9 @@
package org.amnezia.vpn
import android.Manifest
import android.app.AlertDialog
import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Intent
import android.content.Intent.EXTRA_MIME_TYPES
@@ -8,8 +11,8 @@ import android.content.Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.net.Uri
import android.net.VpnService
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.IBinder
@@ -21,6 +24,7 @@ import android.view.WindowManager.LayoutParams
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.annotation.MainThread
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import java.io.IOException
import kotlin.LazyThreadSafetyMode.NONE
@@ -38,6 +42,7 @@ import org.amnezia.vpn.protocol.getStatistics
import org.amnezia.vpn.protocol.getStatus
import org.amnezia.vpn.qt.QtAndroidController
import org.amnezia.vpn.util.Log
import org.amnezia.vpn.util.Prefs
import org.qtproject.qt.android.bindings.QtActivity
private const val TAG = "AmneziaActivity"
@@ -46,6 +51,9 @@ const val ACTIVITY_MESSENGER_NAME = "Activity"
private const val CHECK_VPN_PERMISSION_ACTION_CODE = 1
private const val CREATE_FILE_ACTION_CODE = 2
private const val OPEN_FILE_ACTION_CODE = 3
private const val CHECK_NOTIFICATION_PERMISSION_ACTION_CODE = 4
private const val PREFS_NOTIFICATION_PERMISSION_ASKED = "NOTIFICATION_PERMISSION_ASKED"
class AmneziaActivity : QtActivity() {
@@ -54,8 +62,11 @@ class AmneziaActivity : QtActivity() {
private var isWaitingStatus = true
private var isServiceConnected = false
private var isInBoundState = false
private var notificationStateReceiver: BroadcastReceiver? = null
private lateinit var vpnServiceMessenger: IpcMessenger
private var tmpFileContentToSave: String = ""
private val actionResultHandlers = mutableMapOf<Int, ActivityResultHandler>()
private val permissionRequestHandlers = mutableMapOf<Int, PermissionRequestHandler>()
private val vpnServiceEventHandler: Handler by lazy(NONE) {
object : Handler(Looper.getMainLooper()) {
@@ -135,10 +146,6 @@ class AmneziaActivity : QtActivity() {
}
}
private data class CheckVpnPermissionCallbacks(val onSuccess: () -> Unit, val onFail: () -> Unit)
private var checkVpnPermissionCallbacks: CheckVpnPermissionCallbacks? = null
/**
* Activity overloaded methods
*/
@@ -153,9 +160,30 @@ class AmneziaActivity : QtActivity() {
doBindService()
}
)
registerBroadcastReceivers()
intent?.let(::processIntent)
}
private fun registerBroadcastReceivers() {
notificationStateReceiver = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
registerBroadcastReceiver(
arrayOf(
NotificationManager.ACTION_NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED,
NotificationManager.ACTION_APP_BLOCK_STATE_CHANGED
)
) {
Log.d(
TAG, "Notification state changed: ${it?.action}, blocked = " +
"${it?.getBooleanExtra(NotificationManager.EXTRA_BLOCKED_STATE, false)}"
)
mainScope.launch {
qtInitialized.await()
QtAndroidController.onNotificationStateChanged()
}
}
} else null
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
Log.d(TAG, "onNewIntent: $intent")
@@ -193,50 +221,46 @@ class AmneziaActivity : QtActivity() {
override fun onDestroy() {
Log.d(TAG, "Destroy Amnezia activity")
unregisterBroadcastReceiver(notificationStateReceiver)
notificationStateReceiver = null
mainScope.cancel()
super.onDestroy()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
CREATE_FILE_ACTION_CODE -> {
when (resultCode) {
RESULT_OK -> {
data?.data?.let { uri ->
alterDocument(uri)
}
}
}
Log.d(TAG, "Process activity result, code: ${actionCodeToString(requestCode)}, " +
"resultCode: $resultCode, data: $data")
actionResultHandlers[requestCode]?.let { handler ->
when (resultCode) {
RESULT_OK -> handler.onSuccess(data)
else -> handler.onFail(data)
}
handler.onAny(data)
actionResultHandlers.remove(requestCode)
} ?: super.onActivityResult(requestCode, resultCode, data)
}
OPEN_FILE_ACTION_CODE -> {
when (resultCode) {
RESULT_OK -> data?.data?.toString() ?: ""
else -> ""
}.let { uri ->
QtAndroidController.onFileOpened(uri)
}
private fun startActivityForResult(intent: Intent, requestCode: Int, handler: ActivityResultHandler) {
actionResultHandlers[requestCode] = handler
startActivityForResult(intent, requestCode)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
Log.d(TAG, "Process permission result, code: ${actionCodeToString(requestCode)}, " +
"permissions: ${permissions.contentToString()}, results: ${grantResults.contentToString()}")
permissionRequestHandlers[requestCode]?.let { handler ->
if (grantResults.isNotEmpty()) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) handler.onSuccess()
else handler.onFail()
}
handler.onAny()
permissionRequestHandlers.remove(requestCode)
} ?: super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
CHECK_VPN_PERMISSION_ACTION_CODE -> {
when (resultCode) {
RESULT_OK -> {
Log.d(TAG, "Vpn permission granted")
Toast.makeText(this, resources.getText(R.string.vpnGranted), Toast.LENGTH_LONG).show()
checkVpnPermissionCallbacks?.run { onSuccess() }
}
else -> {
Log.w(TAG, "Vpn permission denied, resultCode: $resultCode")
showOnVpnPermissionRejectDialog()
checkVpnPermissionCallbacks?.run { onFail() }
}
}
checkVpnPermissionCallbacks = null
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
private fun requestPermission(permission: String, requestCode: Int, handler: PermissionRequestHandler) {
permissionRequestHandlers[requestCode] = handler
requestPermissions(arrayOf(permission), requestCode)
}
/**
@@ -268,22 +292,26 @@ class AmneziaActivity : QtActivity() {
/**
* Methods of starting and stopping VpnService
*/
private fun checkVpnPermissionAndStart(vpnConfig: String) {
checkVpnPermission(
onSuccess = { startVpn(vpnConfig) },
onFail = QtAndroidController::onVpnPermissionRejected
)
}
@MainThread
private fun checkVpnPermission(onSuccess: () -> Unit, onFail: () -> Unit) {
private fun checkVpnPermission(onPermissionGranted: () -> Unit) {
Log.d(TAG, "Check VPN permission")
VpnService.prepare(applicationContext)?.let {
checkVpnPermissionCallbacks = CheckVpnPermissionCallbacks(onSuccess, onFail)
startActivityForResult(it, CHECK_VPN_PERMISSION_ACTION_CODE)
return
}
onSuccess()
VpnService.prepare(applicationContext)?.let { intent ->
startActivityForResult(intent, CHECK_VPN_PERMISSION_ACTION_CODE, ActivityResultHandler(
onSuccess = {
Log.d(TAG, "Vpn permission granted")
Toast.makeText(this@AmneziaActivity, resources.getText(R.string.vpnGranted), Toast.LENGTH_LONG).show()
onPermissionGranted()
},
onFail = {
Log.w(TAG, "Vpn permission denied")
showOnVpnPermissionRejectDialog()
mainScope.launch {
qtInitialized.await()
QtAndroidController.onVpnPermissionRejected()
}
}
))
} ?: onPermissionGranted()
}
private fun showOnVpnPermissionRejectDialog() {
@@ -297,6 +325,44 @@ class AmneziaActivity : QtActivity() {
.show()
}
private fun checkNotificationPermission(onChecked: () -> Unit) {
Log.d(TAG, "Check notification permission")
if (
!isNotificationPermissionGranted() &&
!Prefs.load<Boolean>(PREFS_NOTIFICATION_PERMISSION_ASKED)
) {
showNotificationPermissionDialog(onChecked)
} else {
onChecked()
}
}
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private fun showNotificationPermissionDialog(onChecked: () -> Unit) {
AlertDialog.Builder(this)
.setTitle(R.string.notificationDialogTitle)
.setMessage(R.string.notificationDialogMessage)
.setNegativeButton(R.string.no) { _, _ ->
Prefs.save(PREFS_NOTIFICATION_PERMISSION_ASKED, true)
onChecked()
}
.setPositiveButton(R.string.yes) { _, _ ->
val saveAsked: () -> Unit = {
Prefs.save(PREFS_NOTIFICATION_PERMISSION_ASKED, true)
}
requestPermission(
Manifest.permission.POST_NOTIFICATIONS,
CHECK_NOTIFICATION_PERMISSION_ACTION_CODE,
PermissionRequestHandler(
onSuccess = saveAsked,
onFail = saveAsked,
onAny = onChecked
)
)
}
.show()
}
@MainThread
private fun startVpn(vpnConfig: String) {
if (isServiceConnected) {
@@ -322,28 +388,21 @@ class AmneziaActivity : QtActivity() {
Intent(this, AmneziaVpnService::class.java).apply {
putExtra(MSG_VPN_CONFIG, vpnConfig)
}.also {
ContextCompat.startForegroundService(this, it)
try {
ContextCompat.startForegroundService(this, it)
} catch (e: SecurityException) {
Log.e(TAG, "Failed to start AmneziaVpnService: $e")
QtAndroidController.onServiceError()
}
}
}
@MainThread
private fun disconnectFromVpn() {
Log.d(TAG, "Disconnect from VPN")
vpnServiceMessenger.send(Action.DISCONNECT)
}
// saving file
private fun alterDocument(uri: Uri) {
try {
contentResolver.openOutputStream(uri)?.use { os ->
os.bufferedWriter().use { it.write(tmpFileContentToSave) }
}
} catch (e: IOException) {
e.printStackTrace()
}
tmpFileContentToSave = ""
}
/**
* Methods called by Qt
*/
@@ -357,7 +416,11 @@ class AmneziaActivity : QtActivity() {
fun start(vpnConfig: String) {
Log.v(TAG, "Start VPN")
mainScope.launch {
checkVpnPermissionAndStart(vpnConfig)
checkVpnPermission {
checkNotificationPermission {
startVpn(vpnConfig)
}
}
}
}
@@ -389,14 +452,26 @@ class AmneziaActivity : QtActivity() {
fun saveFile(fileName: String, data: String) {
Log.d(TAG, "Save file $fileName")
mainScope.launch {
tmpFileContentToSave = data
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "text/*"
putExtra(Intent.EXTRA_TITLE, fileName)
}.also {
startActivityForResult(it, CREATE_FILE_ACTION_CODE)
startActivityForResult(it, CREATE_FILE_ACTION_CODE, ActivityResultHandler(
onSuccess = {
it?.data?.let { uri ->
Log.d(TAG, "Save file to $uri")
try {
contentResolver.openOutputStream(uri)?.use { os ->
os.bufferedWriter().use { it.write(data) }
}
} catch (e: IOException) {
Log.e(TAG, "Failed to save file $uri: $e")
// todo: send error to Qt
}
}
}
))
}
}
}
@@ -404,42 +479,47 @@ class AmneziaActivity : QtActivity() {
@Suppress("unused")
fun openFile(filter: String?) {
Log.v(TAG, "Open file with filter: $filter")
mainScope.launch {
val mimeTypes = if (!filter.isNullOrEmpty()) {
val extensionRegex = "\\*\\.([a-z0-9]+)".toRegex(IGNORE_CASE)
val mime = MimeTypeMap.getSingleton()
extensionRegex.findAll(filter).map {
it.groups[1]?.value?.let { mime.getMimeTypeFromExtension(it) } ?: "*/*"
}.toSet()
} else emptySet()
val mimeTypes = if (!filter.isNullOrEmpty()) {
val extensionRegex = "\\*\\.([a-z0-9]+)".toRegex(IGNORE_CASE)
val mime = MimeTypeMap.getSingleton()
extensionRegex.findAll(filter).map {
it.groups[1]?.value?.let { mime.getMimeTypeFromExtension(it) } ?: "*/*"
}.toSet()
} else emptySet()
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
Log.v(TAG, "File mimyType filter: $mimeTypes")
if ("*/*" in mimeTypes) {
type = "*/*"
} else {
when (mimeTypes.size) {
1 -> type = mimeTypes.first()
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
Log.v(TAG, "File mimyType filter: $mimeTypes")
if ("*/*" in mimeTypes) {
type = "*/*"
} else {
when (mimeTypes.size) {
1 -> type = mimeTypes.first()
in 2..Int.MAX_VALUE -> {
type = "*/*"
putExtra(EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
}
in 2..Int.MAX_VALUE -> {
type = "*/*"
putExtra(EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
else -> type = "*/*"
}
else -> type = "*/*"
}
}.also {
startActivityForResult(it, OPEN_FILE_ACTION_CODE, ActivityResultHandler(
onSuccess = {
val uri = it?.data?.toString() ?: ""
Log.d(TAG, "Open file: $uri")
mainScope.launch {
qtInitialized.await()
QtAndroidController.onFileOpened(uri)
}
}
))
}
}.also {
startActivityForResult(it, OPEN_FILE_ACTION_CODE)
}
}
@Suppress("unused")
fun setNotificationText(title: String, message: String, timerSec: Int) {
Log.v(TAG, "Set notification text")
}
@Suppress("unused")
fun isCameraPresent(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)
@@ -514,4 +594,76 @@ class AmneziaActivity : QtActivity() {
Log.v(TAG, "Get app icon")
return AppListProvider.getAppIcon(packageManager, packageName, width, height)
}
@Suppress("unused")
fun isNotificationPermissionGranted(): Boolean = applicationContext.isNotificationPermissionGranted()
@Suppress("unused")
fun requestNotificationPermission() {
val shouldShowPreRequest = shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)
requestPermission(
Manifest.permission.POST_NOTIFICATIONS,
CHECK_NOTIFICATION_PERMISSION_ACTION_CODE,
PermissionRequestHandler(
onSuccess = {
mainScope.launch {
Prefs.save(PREFS_NOTIFICATION_PERMISSION_ASKED, true)
vpnServiceMessenger.send(Action.NOTIFICATION_PERMISSION_GRANTED)
qtInitialized.await()
QtAndroidController.onNotificationStateChanged()
}
},
onFail = {
if (!Prefs.load<Boolean>(PREFS_NOTIFICATION_PERMISSION_ASKED)) {
Prefs.save(PREFS_NOTIFICATION_PERMISSION_ASKED, true)
} else {
val shouldShowPostRequest =
shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)
if (!shouldShowPreRequest && !shouldShowPostRequest) {
showNotificationSettingsDialog()
}
}
}
)
)
}
private fun showNotificationSettingsDialog() {
AlertDialog.Builder(this)
.setTitle(R.string.notificationSettingsDialogTitle)
.setMessage(R.string.notificationSettingsDialogMessage)
.setNegativeButton(R.string.cancel) { _, _ -> }
.setPositiveButton(R.string.openNotificationSettings) { _, _ ->
startActivity(Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
})
}
.show()
}
/**
* Utils methods
*/
companion object {
private fun actionCodeToString(actionCode: Int): String =
when (actionCode) {
CHECK_VPN_PERMISSION_ACTION_CODE -> "CHECK_VPN_PERMISSION"
CREATE_FILE_ACTION_CODE -> "CREATE_FILE"
OPEN_FILE_ACTION_CODE -> "OPEN_FILE"
CHECK_NOTIFICATION_PERMISSION_ACTION_CODE -> "CHECK_NOTIFICATION_PERMISSION"
else -> actionCode.toString()
}
}
}
private class ActivityResultHandler(
val onSuccess: (data: Intent?) -> Unit = {},
val onFail: (data: Intent?) -> Unit = {},
val onAny: (data: Intent?) -> Unit = {}
)
private class PermissionRequestHandler(
val onSuccess: () -> Unit = {},
val onFail: () -> Unit = {},
val onAny: () -> Unit = {}
)

View File

@@ -3,14 +3,11 @@ package org.amnezia.vpn
import androidx.camera.camera2.Camera2Config
import androidx.camera.core.CameraSelector
import androidx.camera.core.CameraXConfig
import androidx.core.app.NotificationChannelCompat.Builder
import androidx.core.app.NotificationManagerCompat
import org.amnezia.vpn.util.Log
import org.amnezia.vpn.util.Prefs
import org.qtproject.qt.android.bindings.QtApplication
private const val TAG = "AmneziaApplication"
const val NOTIFICATION_CHANNEL_ID: String = "org.amnezia.vpn.notification"
class AmneziaApplication : QtApplication(), CameraXConfig.Provider {
@@ -20,7 +17,7 @@ class AmneziaApplication : QtApplication(), CameraXConfig.Provider {
Log.init(this)
VpnStateStore.init(this)
Log.d(TAG, "Create Amnezia application")
createNotificationChannel()
ServiceNotification.createNotificationChannel(this)
}
override fun getCameraXConfig(): CameraXConfig = CameraXConfig.Builder
@@ -28,14 +25,4 @@ class AmneziaApplication : QtApplication(), CameraXConfig.Provider {
.setMinimumLoggingLevel(android.util.Log.ERROR)
.setAvailableCamerasLimiter(CameraSelector.DEFAULT_BACK_CAMERA)
.build()
private fun createNotificationChannel() {
NotificationManagerCompat.from(this).createNotificationChannel(
Builder(NOTIFICATION_CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_LOW)
.setName("AmneziaVPN")
.setDescription("AmneziaVPN service notification")
.setShowBadge(false)
.build()
)
}
}

View File

@@ -0,0 +1,56 @@
package org.amnezia.vpn
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.RegisterReceiverFlags
import org.amnezia.vpn.protocol.ProtocolState
import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
import org.amnezia.vpn.protocol.ProtocolState.CONNECTING
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTING
import org.amnezia.vpn.protocol.ProtocolState.RECONNECTING
import org.amnezia.vpn.protocol.ProtocolState.UNKNOWN
fun Context.getString(state: ProtocolState): String =
getString(
when (state) {
DISCONNECTED, UNKNOWN -> R.string.disconnected
CONNECTED -> R.string.connected
CONNECTING -> R.string.connecting
DISCONNECTING -> R.string.disconnecting
RECONNECTING -> R.string.reconnecting
}
)
fun Context.registerBroadcastReceiver(
action: String,
@RegisterReceiverFlags flags: Int = ContextCompat.RECEIVER_EXPORTED,
onReceive: (Intent?) -> Unit
): BroadcastReceiver = registerBroadcastReceiver(arrayOf(action), flags, onReceive)
fun Context.registerBroadcastReceiver(
actions: Array<String>,
@RegisterReceiverFlags flags: Int = ContextCompat.RECEIVER_EXPORTED,
onReceive: (Intent?) -> Unit
): BroadcastReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
onReceive(intent)
}
}.also {
ContextCompat.registerReceiver(
this,
it,
IntentFilter().apply {
actions.forEach(::addAction)
},
flags
)
}
fun Context.unregisterBroadcastReceiver(receiver: BroadcastReceiver?) {
receiver?.let { this.unregisterReceiver(it) }
}

View File

@@ -188,11 +188,16 @@ class AmneziaTileService : TileService() {
true
}
private fun startVpnService() =
ContextCompat.startForegroundService(
applicationContext,
Intent(this, AmneziaVpnService::class.java)
)
private fun startVpnService() {
try {
ContextCompat.startForegroundService(
applicationContext,
Intent(this, AmneziaVpnService::class.java)
)
} catch (e: SecurityException) {
Log.e(TAG, "Failed to start AmneziaVpnService: $e")
}
}
private fun connectToVpn() = vpnServiceMessenger.send(Action.CONNECT)
@@ -230,7 +235,7 @@ class AmneziaTileService : TileService() {
val tile = qsTile ?: return
tile.apply {
label = vpnState.serverName ?: DEFAULT_TILE_LABEL
when (vpnState.protocolState) {
when (val protocolState = vpnState.protocolState) {
CONNECTED -> {
state = Tile.STATE_ACTIVE
subtitleCompat = null
@@ -241,14 +246,9 @@ class AmneziaTileService : TileService() {
subtitleCompat = null
}
CONNECTING, RECONNECTING -> {
CONNECTING, DISCONNECTING, RECONNECTING -> {
state = Tile.STATE_UNAVAILABLE
subtitleCompat = resources.getString(R.string.connecting)
}
DISCONNECTING -> {
state = Tile.STATE_UNAVAILABLE
subtitleCompat = resources.getString(R.string.disconnecting)
subtitleCompat = getString(protocolState)
}
}
updateTile()

View File

@@ -2,8 +2,8 @@ package org.amnezia.vpn
import android.app.ActivityManager
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE
import android.app.Notification
import android.app.PendingIntent
import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
@@ -15,10 +15,12 @@ import android.os.IBinder
import android.os.Looper
import android.os.Message
import android.os.Messenger
import android.os.PowerManager
import android.os.Process
import androidx.annotation.MainThread
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import java.util.concurrent.ConcurrentHashMap
import kotlin.LazyThreadSafetyMode.NONE
import kotlinx.coroutines.CoroutineExceptionHandler
@@ -54,11 +56,14 @@ import org.amnezia.vpn.protocol.wireguard.Wireguard
import org.amnezia.vpn.util.Log
import org.amnezia.vpn.util.Prefs
import org.amnezia.vpn.util.net.NetworkState
import org.amnezia.vpn.util.net.TrafficStats
import org.json.JSONException
import org.json.JSONObject
private const val TAG = "AmneziaVpnService"
const val ACTION_DISCONNECT = "org.amnezia.vpn.action.disconnect"
const val MSG_VPN_CONFIG = "VPN_CONFIG"
const val MSG_ERROR = "ERROR"
const val MSG_SAVE_LOGS = "SAVE_LOGS"
@@ -69,8 +74,8 @@ private const val PREFS_CONFIG_KEY = "LAST_CONF"
private const val PREFS_SERVER_NAME = "LAST_SERVER_NAME"
private const val PREFS_SERVER_INDEX = "LAST_SERVER_INDEX"
private const val PROCESS_NAME = "org.amnezia.vpn:amneziaVpnService"
private const val NOTIFICATION_ID = 1337
private const val STATISTICS_SENDING_TIMEOUT = 1000L
// private const val STATISTICS_SENDING_TIMEOUT = 1000L
private const val TRAFFIC_STATS_UPDATE_TIMEOUT = 1000L
private const val DISCONNECT_TIMEOUT = 5000L
private const val STOP_SERVICE_TIMEOUT = 5000L
@@ -96,8 +101,14 @@ class AmneziaVpnService : VpnService() {
private var connectionJob: Job? = null
private var disconnectionJob: Job? = null
private var statisticsSendingJob: Job? = null
private var trafficStatsUpdateJob: Job? = null
// private var statisticsSendingJob: Job? = null
private lateinit var networkState: NetworkState
private lateinit var trafficStats: TrafficStats
private var disconnectReceiver: BroadcastReceiver? = null
private var notificationStateReceiver: BroadcastReceiver? = null
private var screenOnReceiver: BroadcastReceiver? = null
private var screenOffReceiver: BroadcastReceiver? = null
private val clientMessengers = ConcurrentHashMap<Messenger, IpcMessenger>()
private val isActivityConnected
@@ -131,13 +142,13 @@ class AmneziaVpnService : VpnService() {
val messenger = IpcMessenger(msg.replyTo, clientName)
clientMessengers[msg.replyTo] = messenger
Log.d(TAG, "Messenger client '$clientName' was registered")
if (clientName == ACTIVITY_MESSENGER_NAME && isConnected) launchSendingStatistics()
// if (clientName == ACTIVITY_MESSENGER_NAME && isConnected) launchSendingStatistics()
}
Action.UNREGISTER_CLIENT -> {
clientMessengers.remove(msg.replyTo)?.let {
Log.d(TAG, "Messenger client '${it.name}' was unregistered")
if (it.name == ACTIVITY_MESSENGER_NAME) stopSendingStatistics()
// if (it.name == ACTIVITY_MESSENGER_NAME) stopSendingStatistics()
}
}
@@ -159,6 +170,10 @@ class AmneziaVpnService : VpnService() {
}
}
Action.NOTIFICATION_PERMISSION_GRANTED -> {
enableNotification()
}
Action.SET_SAVE_LOGS -> {
Log.saveLogs = msg.data.getBoolean(MSG_SAVE_LOGS)
}
@@ -181,25 +196,7 @@ class AmneziaVpnService : VpnService() {
else -> 0
}
private val notification: Notification by lazy(NONE) {
NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_amnezia_round)
.setShowWhen(false)
.setContentIntent(
PendingIntent.getActivity(
this,
0,
Intent(this, AmneziaActivity::class.java),
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
)
.setOngoing(true)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.build()
}
private val serviceNotification: ServiceNotification by lazy(NONE) { ServiceNotification(this) }
/**
* Service overloaded methods
@@ -212,6 +209,8 @@ class AmneziaVpnService : VpnService() {
loadServerData()
launchProtocolStateHandler()
networkState = NetworkState(this, ::reconnect)
trafficStats = TrafficStats()
registerBroadcastReceivers()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@@ -227,7 +226,10 @@ class AmneziaVpnService : VpnService() {
Log.d(TAG, "Start service")
connect(intent?.getStringExtra(MSG_VPN_CONFIG))
}
ServiceCompat.startForeground(this, NOTIFICATION_ID, notification, foregroundServiceTypeCompat)
ServiceCompat.startForeground(
this, NOTIFICATION_ID, serviceNotification.buildNotification(serverName, protocolState.value),
foregroundServiceTypeCompat
)
return START_REDELIVER_INTENT
}
@@ -267,6 +269,7 @@ class AmneziaVpnService : VpnService() {
override fun onDestroy() {
Log.d(TAG, "Destroy service")
unregisterBroadcastReceivers()
runBlocking {
disconnect()
disconnectionJob?.join()
@@ -287,6 +290,63 @@ class AmneziaVpnService : VpnService() {
stopSelf()
}
private fun registerBroadcastReceivers() {
Log.d(TAG, "Register broadcast receivers")
disconnectReceiver = registerBroadcastReceiver(ACTION_DISCONNECT, ContextCompat.RECEIVER_NOT_EXPORTED) {
Log.d(TAG, "Broadcast request received: $ACTION_DISCONNECT")
disconnect()
}
notificationStateReceiver = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
registerBroadcastReceiver(
arrayOf(
NotificationManager.ACTION_NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED,
NotificationManager.ACTION_APP_BLOCK_STATE_CHANGED
)
) {
val state = it?.getBooleanExtra(NotificationManager.EXTRA_BLOCKED_STATE, false)
Log.d(TAG, "Notification state changed: ${it?.action}, blocked = $state")
if (state == false) {
enableNotification()
} else {
disableNotification()
}
}
} else null
registerScreenStateBroadcastReceivers()
}
private fun registerScreenStateBroadcastReceivers() {
if (serviceNotification.isNotificationEnabled()) {
Log.d(TAG, "Register screen state broadcast receivers")
screenOnReceiver = registerBroadcastReceiver(Intent.ACTION_SCREEN_ON) {
if (isConnected && serviceNotification.isNotificationEnabled()) startTrafficStatsUpdateJob()
}
screenOffReceiver = registerBroadcastReceiver(Intent.ACTION_SCREEN_OFF) {
stopTrafficStatsUpdateJob()
}
}
}
private fun unregisterScreenStateBroadcastReceivers() {
Log.d(TAG, "Unregister screen state broadcast receivers")
unregisterBroadcastReceiver(screenOnReceiver)
unregisterBroadcastReceiver(screenOffReceiver)
screenOnReceiver = null
screenOffReceiver = null
}
private fun unregisterBroadcastReceivers() {
Log.d(TAG, "Unregister broadcast receivers")
unregisterBroadcastReceiver(disconnectReceiver)
unregisterBroadcastReceiver(notificationStateReceiver)
unregisterScreenStateBroadcastReceivers()
disconnectReceiver = null
notificationStateReceiver = null
}
/**
* Methods responsible for processing VPN connection
*/
@@ -295,29 +355,8 @@ class AmneziaVpnService : VpnService() {
// drop first default UNKNOWN state
protocolState.drop(1).collect { protocolState ->
Log.d(TAG, "Protocol state changed: $protocolState")
when (protocolState) {
CONNECTED -> {
networkState.bindNetworkListener()
if (isActivityConnected) launchSendingStatistics()
}
DISCONNECTED -> {
networkState.unbindNetworkListener()
stopSendingStatistics()
if (!isServiceBound) stopService()
}
DISCONNECTING -> {
networkState.unbindNetworkListener()
stopSendingStatistics()
}
RECONNECTING -> {
stopSendingStatistics()
}
CONNECTING, UNKNOWN -> {}
}
serviceNotification.updateNotification(serverName, protocolState)
clientMessengers.send {
ServiceEvent.STATUS_CHANGED.packToMessage {
@@ -326,13 +365,41 @@ class AmneziaVpnService : VpnService() {
}
VpnStateStore.store { VpnState(protocolState, serverName, serverIndex) }
when (protocolState) {
CONNECTED -> {
networkState.bindNetworkListener()
// if (isActivityConnected) launchSendingStatistics()
launchTrafficStatsUpdate()
}
DISCONNECTED -> {
networkState.unbindNetworkListener()
stopTrafficStatsUpdateJob()
// stopSendingStatistics()
if (!isServiceBound) stopService()
}
DISCONNECTING -> {
networkState.unbindNetworkListener()
stopTrafficStatsUpdateJob()
// stopSendingStatistics()
}
RECONNECTING -> {
stopTrafficStatsUpdateJob()
// stopSendingStatistics()
}
CONNECTING, UNKNOWN -> {}
}
}
}
}
@MainThread
/* @MainThread
private fun launchSendingStatistics() {
/* if (isServiceBound && isConnected) {
if (isServiceBound && isConnected) {
statisticsSendingJob = mainScope.launch {
while (true) {
clientMessenger.send {
@@ -343,12 +410,62 @@ class AmneziaVpnService : VpnService() {
delay(STATISTICS_SENDING_TIMEOUT)
}
}
} */
}
}
@MainThread
private fun stopSendingStatistics() {
statisticsSendingJob?.cancel()
} */
@MainThread
private fun enableNotification() {
registerScreenStateBroadcastReceivers()
serviceNotification.updateNotification(serverName, protocolState.value)
launchTrafficStatsUpdate()
}
@MainThread
private fun disableNotification() {
unregisterScreenStateBroadcastReceivers()
stopTrafficStatsUpdateJob()
}
@MainThread
private fun launchTrafficStatsUpdate() {
stopTrafficStatsUpdateJob()
if (isConnected &&
serviceNotification.isNotificationEnabled() &&
getSystemService<PowerManager>()?.isInteractive != false
) {
Log.d(TAG, "Launch traffic stats update")
trafficStats.reset()
startTrafficStatsUpdateJob()
}
}
@MainThread
private fun startTrafficStatsUpdateJob() {
if (trafficStatsUpdateJob == null && trafficStats.isSupported()) {
Log.d(TAG, "Start traffic stats update")
trafficStatsUpdateJob = mainScope.launch {
while (true) {
trafficStats.getSpeed().let { speed ->
if (isConnected) {
serviceNotification.updateSpeed(speed)
}
}
delay(TRAFFIC_STATS_UPDATE_TIMEOUT)
}
}
}
}
@MainThread
private fun stopTrafficStatsUpdateJob() {
Log.d(TAG, "Stop traffic stats update")
trafficStatsUpdateJob?.cancel()
trafficStatsUpdateJob = null
}
@MainThread
@@ -471,6 +588,7 @@ class AmneziaVpnService : VpnService() {
private fun saveServerData(config: JSONObject?) {
serverName = config?.opt("description") as String?
serverIndex = config?.opt("serverIndex") as Int? ?: -1
Log.d(TAG, "Save server data: ($serverIndex, $serverName)")
Prefs.save(PREFS_SERVER_NAME, serverName)
Prefs.save(PREFS_SERVER_INDEX, serverIndex)
}
@@ -478,6 +596,7 @@ class AmneziaVpnService : VpnService() {
private fun loadServerData() {
serverName = Prefs.load<String>(PREFS_SERVER_NAME).ifBlank { null }
if (serverName != null) serverIndex = Prefs.load(PREFS_SERVER_INDEX)
Log.d(TAG, "Load server data: ($serverIndex, $serverName)")
}
private fun checkPermission(): Boolean =
@@ -494,9 +613,8 @@ class AmneziaVpnService : VpnService() {
companion object {
fun isRunning(context: Context): Boolean =
(context.getSystemService(ACTIVITY_SERVICE) as ActivityManager)
.runningAppProcesses.any {
it.processName == PROCESS_NAME && it.importance <= IMPORTANCE_FOREGROUND_SERVICE
}
context.getSystemService<ActivityManager>()!!.runningAppProcesses.any {
it.processName == PROCESS_NAME && it.importance <= IMPORTANCE_FOREGROUND_SERVICE
}
}
}

View File

@@ -32,6 +32,7 @@ enum class Action : IpcMessage {
CONNECT,
DISCONNECT,
REQUEST_STATUS,
NOTIFICATION_PERMISSION_GRANTED,
SET_SAVE_LOGS
}

View File

@@ -1,189 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.amnezia.vpn;
import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.Manifest.permission;
import android.net.Uri;
import android.os.Build;
import android.util.Log;
import android.webkit.WebView;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
// Gets used by /platforms/android/androidAppListProvider.cpp
public class PackageManagerHelper {
final static String TAG = "PackageManagerHelper";
final static int MIN_CHROME_VERSION = 65;
final static List<String> CHROME_BROWSERS = Arrays.asList(
new String[] {"com.google.android.webview", "com.android.webview", "com.google.chrome"});
private static String getAllAppNames(Context ctx) {
JSONObject output = new JSONObject();
PackageManager pm = ctx.getPackageManager();
List<String> browsers = getBrowserIDs(pm);
List<PackageInfo> packs = pm.getInstalledPackages(PackageManager.GET_PERMISSIONS);
for (int i = 0; i < packs.size(); i++) {
PackageInfo p = packs.get(i);
// Do not add ourselves and System Apps to the list, unless it might be a browser
if ((!isSystemPackage(p,pm) || browsers.contains(p.packageName))
&& !isSelf(p)) {
String appid = p.packageName;
String appName = p.applicationInfo.loadLabel(pm).toString();
try {
output.put(appid, appName);
} catch (JSONException e) {
e.printStackTrace();
}
}
}
return output.toString();
}
private static Drawable getAppIcon(Context ctx, String id) {
try {
return ctx.getPackageManager().getApplicationIcon(id);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return new ColorDrawable(Color.TRANSPARENT);
}
private static boolean isSystemPackage(PackageInfo pkgInfo, PackageManager pm) {
if( (pkgInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0){
// no system app
return false;
}
// For Systems Packages there are Cases where we want to add it anyway:
// Has the use Internet permission (otherwise makes no sense)
// Had at least 1 update (this means it's probably on any AppStore)
// Has a a launch activity (has a ui and is not just a system service)
if(!usesInternet(pkgInfo)){
return true;
}
if(!hadUpdate(pkgInfo)){
return true;
}
if(pm.getLaunchIntentForPackage(pkgInfo.packageName) == null){
// If there is no way to launch this from a homescreen, def a sys package
return true;
}
return false;
}
private static boolean isSelf(PackageInfo pkgInfo) {
return pkgInfo.packageName.equals("org.amnezia.vpn")
|| pkgInfo.packageName.equals("org.amnezia.vpn.debug");
}
private static boolean usesInternet(PackageInfo pkgInfo){
if(pkgInfo.requestedPermissions == null){
return false;
}
for(int i=0; i < pkgInfo.requestedPermissions.length; i++) {
String permission = pkgInfo.requestedPermissions[i];
if(Manifest.permission.INTERNET.equals(permission)){
return true;
}
}
return false;
}
private static boolean hadUpdate(PackageInfo pkgInfo){
return pkgInfo.lastUpdateTime > pkgInfo.firstInstallTime;
}
// Returns List of all Packages that can classify themselves as browsers
private static List<String> getBrowserIDs(PackageManager pm) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://www.amnezia.org/"));
intent.addCategory(Intent.CATEGORY_BROWSABLE);
// We've tried using PackageManager.MATCH_DEFAULT_ONLY flag and found that browsers that
// are not set as the default browser won't be matched even if they had CATEGORY_DEFAULT set
// in the intent filter
List<ResolveInfo> resolveInfos = pm.queryIntentActivities(intent, PackageManager.MATCH_ALL);
List<String> browsers = new ArrayList<String>();
for (int i = 0; i < resolveInfos.size(); i++) {
ResolveInfo info = resolveInfos.get(i);
String browserID = info.activityInfo.packageName;
browsers.add(browserID);
}
return browsers;
}
// Gets called in AndroidAuthenticationListener;
public static boolean isWebViewSupported(Context ctx) {
Log.v(TAG, "Checking if installed Webview is compatible with FxA");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// The default Webview is able do to FXA
return true;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PackageInfo pi = WebView.getCurrentWebViewPackage();
if (CHROME_BROWSERS.contains(pi.packageName)) {
return isSupportedChromeBrowser(pi);
}
return isNotAncientBrowser(pi);
}
// Before O the webview is hardcoded, but we dont know which package it is.
// Check if com.google.android.webview is installed
PackageManager pm = ctx.getPackageManager();
try {
PackageInfo pi = pm.getPackageInfo("com.google.android.webview", 0);
return isSupportedChromeBrowser(pi);
} catch (PackageManager.NameNotFoundException e) {
}
// Otherwise check com.android.webview
try {
PackageInfo pi = pm.getPackageInfo("com.android.webview", 0);
return isSupportedChromeBrowser(pi);
} catch (PackageManager.NameNotFoundException e) {
}
Log.e(TAG, "Android System WebView is not found");
// Giving up :(
return false;
}
private static boolean isSupportedChromeBrowser(PackageInfo pi) {
Log.d(TAG, "Checking Chrome Based Browser: " + pi.packageName);
Log.d(TAG, "version name: " + pi.versionName);
Log.d(TAG, "version code: " + pi.versionCode);
try {
String versionCode = pi.versionName.split(Pattern.quote(" "))[0];
String majorVersion = versionCode.split(Pattern.quote("."))[0];
int version = Integer.parseInt(majorVersion);
return version >= MIN_CHROME_VERSION;
} catch (Exception e) {
Log.e(TAG, "Failed to check Chrome Version Code " + pi.versionName);
return false;
}
}
private static boolean isNotAncientBrowser(PackageInfo pi) {
// Not a google chrome - So the version name is worthless
// Lets just make sure the WebView
// used is not ancient ==> Was updated in at least the last 365 days
Log.d(TAG, "Checking Chrome Based Browser: " + pi.packageName);
Log.d(TAG, "version name: " + pi.versionName);
Log.d(TAG, "version code: " + pi.versionCode);
double oneYearInMillis = 31536000000L;
return pi.lastUpdateTime > (System.currentTimeMillis() - oneYearInMillis);
}
}

View File

@@ -0,0 +1,180 @@
package org.amnezia.vpn
import android.Manifest.permission
import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.app.NotificationChannelCompat.Builder
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.Action
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import org.amnezia.vpn.protocol.ProtocolState
import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
import org.amnezia.vpn.util.Log
import org.amnezia.vpn.util.net.TrafficStats.TrafficData
private const val TAG = "ServiceNotification"
private const val OLD_NOTIFICATION_CHANNEL_ID: String = "org.amnezia.vpn.notification"
private const val NOTIFICATION_CHANNEL_ID: String = "org.amnezia.vpn.notifications"
const val NOTIFICATION_ID = 1337
private const val GET_ACTIVITY_REQUEST_CODE = 0
private const val CONNECT_REQUEST_CODE = 1
private const val DISCONNECT_REQUEST_CODE = 2
class ServiceNotification(private val context: Context) {
private val upDownSymbols = when (Build.BRAND) {
"Infinix" -> '˅' to '˄'
else -> '↓' to '↑'
}
private val notificationManager = NotificationManagerCompat.from(context)
private val notificationBuilder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
.setShowWhen(false)
.setOngoing(true)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setContentIntent(
PendingIntent.getActivity(
context,
GET_ACTIVITY_REQUEST_CODE,
Intent(context, AmneziaActivity::class.java),
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
)
private val zeroSpeed: String = with(TrafficData.ZERO) {
formatSpeedString(rxString, txString)
}
fun buildNotification(serverName: String?, state: ProtocolState): Notification {
val speedString = if (state == CONNECTED) zeroSpeed else null
Log.d(TAG, "Build notification: $serverName, $state")
return notificationBuilder
.setSmallIcon(R.drawable.ic_amnezia_round)
.setContentTitle(serverName ?: "AmneziaVPN")
.setContentText(context.getString(state))
.setSubText(speedString)
.setWhen(System.currentTimeMillis())
.clearActions()
.apply {
getAction(state)?.let {
addAction(it)
}
}
.build()
}
private fun buildNotification(speed: TrafficData): Notification =
notificationBuilder
.setWhen(System.currentTimeMillis())
.setSubText(getSpeedString(speed))
.build()
fun isNotificationEnabled(): Boolean {
if (!context.isNotificationPermissionGranted()) return false
if (!notificationManager.areNotificationsEnabled()) return false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID)
?.let { it.importance != NotificationManager.IMPORTANCE_NONE } ?: true
}
return true
}
@SuppressLint("MissingPermission")
fun updateNotification(serverName: String?, state: ProtocolState) {
if (context.isNotificationPermissionGranted()) {
Log.d(TAG, "Update notification: $serverName, $state")
notificationManager.notify(NOTIFICATION_ID, buildNotification(serverName, state))
}
}
@SuppressLint("MissingPermission")
fun updateSpeed(speed: TrafficData) {
if (context.isNotificationPermissionGranted()) {
notificationManager.notify(NOTIFICATION_ID, buildNotification(speed))
}
}
private fun getSpeedString(traffic: TrafficData) =
if (traffic == TrafficData.ZERO) zeroSpeed
else formatSpeedString(traffic.rxString, traffic.txString)
private fun formatSpeedString(rx: String, tx: String) = with(upDownSymbols) { "$first $rx $second $tx" }
private fun getAction(state: ProtocolState): Action? {
return when (state) {
CONNECTED -> {
Action(
0, context.getString(R.string.disconnect),
PendingIntent.getBroadcast(
context,
DISCONNECT_REQUEST_CODE,
Intent(ACTION_DISCONNECT).apply {
setPackage("org.amnezia.vpn")
},
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
)
}
DISCONNECTED -> {
Action(
0, context.getString(R.string.connect),
createServicePendingIntent(
context,
CONNECT_REQUEST_CODE,
Intent(context, AmneziaVpnService::class.java),
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
)
}
else -> null
}
}
private val createServicePendingIntent: (Context, Int, Intent, Int) -> PendingIntent =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PendingIntent::getForegroundService
} else {
PendingIntent::getService
}
companion object {
fun createNotificationChannel(context: Context) {
with(NotificationManagerCompat.from(context)) {
deleteNotificationChannel(OLD_NOTIFICATION_CHANNEL_ID)
createNotificationChannel(
Builder(NOTIFICATION_CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_DEFAULT)
.setShowBadge(false)
.setSound(null, null)
.setVibrationEnabled(false)
.setLightsEnabled(false)
.setName("AmneziaVPN")
.setDescription(context.resources.getString(R.string.notificationChannelDescription))
.build()
)
}
}
}
}
fun Context.isNotificationPermissionGranted(): Boolean =
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
ContextCompat.checkSelfPermission(this, permission.POST_NOTIFICATIONS) ==
PackageManager.PERMISSION_GRANTED

View File

@@ -3,9 +3,7 @@ package org.amnezia.vpn
import android.app.AlertDialog
import android.app.KeyguardManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.res.Configuration.UI_MODE_NIGHT_MASK
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.net.VpnService
@@ -33,11 +31,9 @@ class VpnRequestActivity : ComponentActivity() {
val requestIntent = VpnService.prepare(applicationContext)
if (requestIntent != null) {
if (getSystemService<KeyguardManager>()!!.isKeyguardLocked) {
userPresentReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) =
requestLauncher.launch(requestIntent)
userPresentReceiver = registerBroadcastReceiver(Intent.ACTION_USER_PRESENT) {
requestLauncher.launch(requestIntent)
}
registerReceiver(userPresentReceiver, IntentFilter(Intent.ACTION_USER_PRESENT))
} else {
requestLauncher.launch(requestIntent)
}
@@ -49,9 +45,8 @@ class VpnRequestActivity : ComponentActivity() {
}
override fun onDestroy() {
userPresentReceiver?.let {
unregisterReceiver(it)
}
unregisterBroadcastReceiver(userPresentReceiver)
userPresentReceiver = null
super.onDestroy()
}

View File

@@ -4,6 +4,8 @@ import android.app.Application
import androidx.datastore.core.MultiProcessDataStoreFactory
import androidx.datastore.core.Serializer
import androidx.datastore.dataStoreFile
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
@@ -59,7 +61,8 @@ private class VpnStateSerializer : Serializer<VpnState> {
override suspend fun readFrom(input: InputStream): VpnState {
return withContext(Dispatchers.IO) {
ObjectInputStream(input).use {
val bios = ByteArrayInputStream(input.readBytes())
ObjectInputStream(bios).use {
it.readObject() as VpnState
}
}
@@ -67,9 +70,11 @@ private class VpnStateSerializer : Serializer<VpnState> {
override suspend fun writeTo(t: VpnState, output: OutputStream) {
withContext(Dispatchers.IO) {
ObjectOutputStream(output).use {
val baos = ByteArrayOutputStream()
ObjectOutputStream(baos).use {
it.writeObject(t)
}
output.write(baos.toByteArray())
}
}
}

View File

@@ -17,6 +17,7 @@ object QtAndroidController {
external fun onServiceError()
external fun onVpnPermissionRejected()
external fun onNotificationStateChanged()
external fun onVpnStateChanged(stateCode: Int)
external fun onStatisticsUpdate(rxBytes: Long, txBytes: Long)

View File

@@ -17,5 +17,7 @@ android {
}
dependencies {
implementation(libs.androidx.core)
implementation(libs.kotlinx.coroutines)
implementation(libs.androidx.security.crypto)
}

View File

@@ -1,16 +1,21 @@
package org.amnezia.vpn.util.net
import java.net.Inet4Address
import java.net.InetAddress
data class InetEndpoint(val address: InetAddress, val port: Int) {
override fun toString(): String = "${address.hostAddress}:$port"
override fun toString(): String = if (address is Inet4Address) {
"${address.ip}:$port"
} else {
"[${address.ip}]:$port"
}
companion object {
fun parse(data: String): InetEndpoint {
val split = data.split(":")
val address = parseInetAddress(split.first())
val port = split.last().toInt()
val i = data.lastIndexOf(':')
val address = parseInetAddress(data.substring(0, i))
val port = data.substring(i + 1).toInt()
return InetEndpoint(address, port)
}
}

View File

@@ -9,7 +9,11 @@ data class InetNetwork(val address: InetAddress, val mask: Int) {
constructor(address: InetAddress) : this(address, address.maxPrefixLength)
override fun toString(): String = "${address.hostAddress}/$mask"
val isIpv4: Boolean = address is Inet4Address
val isIpv6: Boolean
get() = !isIpv4
override fun toString(): String = "${address.ip}/$mask"
companion object {
fun parse(data: String): InetNetwork {

View File

@@ -3,12 +3,17 @@ package org.amnezia.vpn.util.net
import java.net.InetAddress
@OptIn(ExperimentalUnsignedTypes::class)
class IpAddress private constructor(private val address: UByteArray) : Comparable<IpAddress> {
internal class IpAddress private constructor(private val address: UByteArray) : Comparable<IpAddress> {
val size: Int = address.size
val lastIndex: Int = address.lastIndex
val maxMask: Int = size * 8
@OptIn(ExperimentalStdlibApi::class)
val hexFormat: HexFormat by lazy {
HexFormat { number.removeLeadingZeros = true }
}
constructor(inetAddress: InetAddress) : this(inetAddress.address.asUByteArray())
constructor(ipAddress: String) : this(parseInetAddress(ipAddress))
@@ -43,6 +48,8 @@ class IpAddress private constructor(private val address: UByteArray) : Comparabl
return copy
}
fun isMinIp(): Boolean = address.all { it == 0x00u.toUByte() }
fun isMaxIp(): Boolean = address.all { it == 0xffu.toUByte() }
override fun compareTo(other: IpAddress): Int {
@@ -74,12 +81,14 @@ class IpAddress private constructor(private val address: UByteArray) : Comparabl
private fun toIpv6String(): String {
val sb = StringBuilder()
var i = 0
var block: Int
while (i < size) {
sb.append(address[i++].toHexString())
sb.append(address[i++].toHexString())
block = address[i++].toInt() shl 8
block += address[i++].toInt()
sb.append(block.toHexString(hexFormat))
sb.append(':')
}
sb.deleteAt(sb.lastIndex)
return sb.toString()
return convertIpv6ToCanonicalForm(sb.toString())
}
}

View File

@@ -2,14 +2,24 @@ package org.amnezia.vpn.util.net
import java.net.InetAddress
class IpRange(private val start: IpAddress, private val end: IpAddress) : Comparable<IpRange> {
class IpRange internal constructor(
internal val start: IpAddress,
internal val end: IpAddress
) : Comparable<IpRange> {
init {
if (start > end) throw IllegalArgumentException("Start IP: $start is greater then end IP: $end")
if (start.size != end.size) {
throw IllegalArgumentException(
"Unable to create a range between IPv4 and IPv6 addresses (start IP: [$start], end IP: [$end])"
)
}
if (start > end) throw IllegalArgumentException("Start IP: [$start] is greater then end IP: [$end]")
}
private constructor(addresses: Pair<IpAddress, IpAddress>) : this(addresses.first, addresses.second)
internal constructor(ipAddress: IpAddress) : this(ipAddress, ipAddress)
constructor(inetAddress: InetAddress, mask: Int) : this(from(inetAddress, mask))
constructor(address: String, mask: Int) : this(parseInetAddress(address), mask)
@@ -22,6 +32,13 @@ class IpRange(private val start: IpAddress, private val end: IpAddress) : Compar
private fun isIntersect(other: IpRange): Boolean =
(start <= other.end) && (end >= other.start)
operator fun plus(other: IpRange): IpRange? {
if (start > other.end && !start.isMinIp() && start.dec() == other.end) return IpRange(other.start, end)
if (end < other.start && !end.isMaxIp() && end.inc() == other.start) return IpRange(start, other.end)
if (!isIntersect(other)) return null
return IpRange(minOf(start, other.start), maxOf(end, other.end))
}
operator fun minus(other: IpRange): List<IpRange>? {
if (this in other) return emptyList()
if (!isIntersect(other)) return null
@@ -94,9 +111,7 @@ class IpRange(private val start: IpAddress, private val end: IpAddress) : Compar
return result
}
override fun toString(): String {
return "$start - $end"
}
override fun toString(): String = if (start == end) "<$start>" else "<$start - $end>"
companion object {
private fun from(inetAddress: InetAddress, mask: Int): Pair<IpAddress, IpAddress> {

View File

@@ -1,15 +1,35 @@
package org.amnezia.vpn.util.net
class IpRangeSet(ipRange: IpRange = IpRange("0.0.0.0", 0)) {
class IpRangeSet {
private val ranges = sortedSetOf(ipRange)
private val ranges = sortedSetOf<IpRange>()
fun add(ipRange: IpRange) {
val iterator = ranges.iterator()
var rangeToAdd = ipRange
run {
while (iterator.hasNext()) {
val curRange = iterator.next()
if (rangeToAdd.end < curRange.start &&
!rangeToAdd.end.isMaxIp() &&
rangeToAdd.end.inc() != curRange.start) break
(curRange + rangeToAdd)?.let { resultRange ->
if (resultRange == curRange) return@run
iterator.remove()
rangeToAdd = resultRange
}
}
ranges += rangeToAdd
}
}
fun remove(ipRange: IpRange) {
val iterator = ranges.iterator()
val splitRanges = mutableListOf<IpRange>()
while (iterator.hasNext()) {
val range = iterator.next()
(range - ipRange)?.let { resultRanges ->
val curRange = iterator.next()
if (ipRange.end < curRange.start) break
(curRange - ipRange)?.let { resultRanges ->
iterator.remove()
splitRanges += resultRanges
}
@@ -17,10 +37,7 @@ class IpRangeSet(ipRange: IpRange = IpRange("0.0.0.0", 0)) {
ranges += splitRanges
}
fun subnets(): List<InetNetwork> =
ranges.map(IpRange::subnets).flatten()
fun subnets(): List<InetNetwork> = ranges.map(IpRange::subnets).flatten()
override fun toString(): String {
return ranges.toString()
}
override fun toString(): String = ranges.toString()
}

View File

@@ -10,7 +10,9 @@ import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
import android.net.NetworkRequest
import android.os.Build
import android.os.Handler
import androidx.core.content.getSystemService
import kotlin.LazyThreadSafetyMode.NONE
import kotlinx.coroutines.delay
import org.amnezia.vpn.util.Log
private const val TAG = "NetworkState"
@@ -28,7 +30,7 @@ class NetworkState(
}
private val connectivityManager: ConnectivityManager by lazy(NONE) {
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
context.getSystemService<ConnectivityManager>()!!
}
private val networkRequest: NetworkRequest by lazy(NONE) {
@@ -80,13 +82,24 @@ class NetworkState(
}
}
fun bindNetworkListener() {
suspend fun bindNetworkListener() {
if (isListenerBound) return
Log.d(TAG, "Bind network listener")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
connectivityManager.registerBestMatchingNetworkCallback(networkRequest, networkCallback, handler)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
connectivityManager.requestNetwork(networkRequest, networkCallback, handler)
try {
connectivityManager.requestNetwork(networkRequest, networkCallback, handler)
} catch (e: SecurityException) {
Log.e(TAG, "Failed to bind network listener: $e")
// Android 11 bug: https://issuetracker.google.com/issues/175055271
if (e.message?.startsWith("Package android does not belong to") == true) {
delay(1000)
connectivityManager.requestNetwork(networkRequest, networkCallback, handler)
} else {
throw e
}
}
} else {
connectivityManager.requestNetwork(networkRequest, networkCallback)
}

View File

@@ -5,12 +5,14 @@ import android.net.ConnectivityManager
import android.net.InetAddresses
import android.net.NetworkCapabilities
import android.os.Build
import androidx.core.content.getSystemService
import java.lang.reflect.InvocationTargetException
import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress
fun getLocalNetworks(context: Context, ipv6: Boolean): List<InetNetwork> {
val connectivityManager = context.getSystemService(ConnectivityManager::class.java)
val connectivityManager = context.getSystemService<ConnectivityManager>()!!
connectivityManager.activeNetwork?.let { network ->
val netCapabilities = connectivityManager.getNetworkCapabilities(network)
val linkProperties = connectivityManager.getLinkProperties(network)
@@ -39,8 +41,28 @@ private val parseNumericAddressCompat: (String) -> InetAddress =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
InetAddresses::parseNumericAddress
} else {
val m = InetAddress::class.java.getMethod("parseNumericAddress", String::class.java)
fun(address: String): InetAddress {
return m.invoke(null, address) as InetAddress
try {
val m = InetAddress::class.java.getMethod("parseNumericAddress", String::class.java)
fun(address: String): InetAddress {
try {
return m.invoke(null, address) as InetAddress
} catch (e: InvocationTargetException) {
throw e.cause ?: e
}
}
} catch (_: NoSuchMethodException) {
fun(address: String): InetAddress {
return InetAddress.getByName(address)
}
}
}
internal fun convertIpv6ToCanonicalForm(ipv6: String): String = ipv6
.replace("((?:(?:^|:)0+\\b){2,}):?(?!\\S*\\b\\1:0+\\b)(\\S*)".toRegex(), "::$2")
internal val InetAddress.ip: String
get() = if (this is Inet4Address) {
hostAddress!!
} else {
convertIpv6ToCanonicalForm(hostAddress!!)
}

View File

@@ -0,0 +1,93 @@
package org.amnezia.vpn.util.net
import android.net.TrafficStats
import android.os.Build
import android.os.Process
import android.os.SystemClock
import kotlin.math.roundToLong
private const val BYTE = 1L
private const val KiB = BYTE shl 10
private const val MiB = KiB shl 10
private const val GiB = MiB shl 10
private const val TiB = GiB shl 10
class TrafficStats {
private var lastTrafficData = TrafficData.ZERO
private var lastTimestamp = 0L
private val getTrafficDataCompat: () -> TrafficData =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val iface = "tun0"
fun(): TrafficData {
return TrafficData(TrafficStats.getRxBytes(iface), TrafficStats.getTxBytes(iface))
}
} else {
val uid = Process.myUid()
fun(): TrafficData {
return TrafficData(TrafficStats.getUidRxBytes(uid), TrafficStats.getUidTxBytes(uid))
}
}
fun reset() {
lastTrafficData = getTrafficDataCompat()
lastTimestamp = SystemClock.elapsedRealtime()
}
fun isSupported(): Boolean =
lastTrafficData.rx != TrafficStats.UNSUPPORTED.toLong() && lastTrafficData.tx != TrafficStats.UNSUPPORTED.toLong()
fun getSpeed(): TrafficData {
val timestamp = SystemClock.elapsedRealtime()
val elapsedSeconds = (timestamp - lastTimestamp) / 1000.0
val trafficData = getTrafficDataCompat()
val speed = trafficData.diff(lastTrafficData, elapsedSeconds)
lastTrafficData = trafficData
lastTimestamp = timestamp
return speed
}
class TrafficData(val rx: Long, val tx: Long) {
private var _rxString: String? = null
val rxString: String
get() {
if (_rxString == null) _rxString = rx.speedToString()
return _rxString ?: throw AssertionError("Set to null by another thread")
}
private var _txString: String? = null
val txString: String
get() {
if (_txString == null) _txString = tx.speedToString()
return _txString ?: throw AssertionError("Set to null by another thread")
}
fun diff(other: TrafficData, elapsedSeconds: Double): TrafficData {
val rx = ((this.rx - other.rx) / elapsedSeconds).round()
val tx = ((this.tx - other.tx) / elapsedSeconds).round()
return if (rx == 0L && tx == 0L) ZERO else TrafficData(rx, tx)
}
private fun Double.round() = if (isNaN()) 0L else roundToLong()
private fun Long.speedToString() =
when {
this < KiB -> formatSize(this, BYTE, "B/s")
this < MiB -> formatSize(this, KiB, "KiB/s")
this < GiB -> formatSize(this, MiB, "MiB/s")
this < TiB -> formatSize(this, GiB, "GiB/s")
else -> formatSize(this, TiB, "TiB/s")
}
private fun formatSize(bytes: Long, divider: Long, unit: String): String {
val s = (bytes.toDouble() / divider * 100).roundToLong() / 100.0
return "${s.toString().removeSuffix(".0")} $unit"
}
companion object {
val ZERO: TrafficData = TrafficData(0L, 0L)
}
}
}

View File

@@ -37,8 +37,8 @@ open class WireguardConfig protected constructor(
open fun appendPeerLine(sb: StringBuilder) = with(sb) {
appendLine("public_key=$publicKeyHex")
routes.forEach { route ->
appendLine("allowed_ip=$route")
routes.filter { it.include }.forEach { route ->
appendLine("allowed_ip=${route.inetNetwork}")
}
appendLine("endpoint=$endpoint")
if (persistentKeepalive != 0)

View File

@@ -26,7 +26,6 @@ link_directories(${CMAKE_CURRENT_SOURCE_DIR}/platforms/android)
set(HEADERS ${HEADERS}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_controller.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_notificationhandler.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_utils.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/authResultReceiver.h
${CMAKE_CURRENT_SOURCE_DIR}/protocols/android_vpnprotocol.h
@@ -35,7 +34,6 @@ set(HEADERS ${HEADERS}
set(SOURCES ${SOURCES}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_controller.cpp
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_notificationhandler.cpp
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_utils.cpp
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/authResultReceiver.cpp
${CMAKE_CURRENT_SOURCE_DIR}/protocols/android_vpnprotocol.cpp

View File

@@ -46,7 +46,6 @@ set(SOURCES ${SOURCES}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosglue.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QRCodeReaderBase.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/QtAppDelegate.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/MobileUtils.mm
)

View File

@@ -9,7 +9,7 @@ AwgConfigurator::AwgConfigurator(std::shared_ptr<Settings> settings, const QShar
}
QString AwgConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig,
ErrorCode errorCode)
ErrorCode &errorCode)
{
QString config = WireguardConfigurator::createConfig(credentials, container, containerConfig, errorCode);

View File

@@ -12,7 +12,7 @@ public:
AwgConfigurator(std::shared_ptr<Settings> settings, const QSharedPointer<ServerController> &serverController, QObject *parent = nullptr);
QString createConfig(const ServerCredentials &credentials, DockerContainer container,
const QJsonObject &containerConfig, ErrorCode errorCode);
const QJsonObject &containerConfig, ErrorCode &errorCode);
};
#endif // AWGCONFIGURATOR_H

View File

@@ -13,7 +13,7 @@ CloakConfigurator::CloakConfigurator(std::shared_ptr<Settings> settings, const Q
}
QString CloakConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig,
ErrorCode errorCode)
ErrorCode &errorCode)
{
QString cloakPublicKey =
m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::cloak::ckPublicKeyPath, errorCode);

View File

@@ -14,7 +14,7 @@ public:
CloakConfigurator(std::shared_ptr<Settings> settings, const QSharedPointer<ServerController> &serverController, QObject *parent = nullptr);
QString createConfig(const ServerCredentials &credentials, DockerContainer container,
const QJsonObject &containerConfig, ErrorCode errorCode);
const QJsonObject &containerConfig, ErrorCode &errorCode);
};
#endif // CLOAK_CONFIGURATOR_H

View File

@@ -15,7 +15,7 @@ public:
explicit ConfiguratorBase(std::shared_ptr<Settings> settings, const QSharedPointer<ServerController> &serverController, QObject *parent = nullptr);
virtual QString createConfig(const ServerCredentials &credentials, DockerContainer container,
const QJsonObject &containerConfig, ErrorCode errorCode) = 0;
const QJsonObject &containerConfig, ErrorCode &errorCode) = 0;
virtual QString processConfigWithLocalSettings(const QPair<QString, QString> &dns, const bool isApiConfig,
QString &protocolConfigString);

View File

@@ -20,7 +20,7 @@ Ikev2Configurator::Ikev2Configurator(std::shared_ptr<Settings> settings, const Q
}
Ikev2Configurator::ConnectionData Ikev2Configurator::prepareIkev2Config(const ServerCredentials &credentials, DockerContainer container,
ErrorCode errorCode)
ErrorCode &errorCode)
{
Ikev2Configurator::ConnectionData connData;
connData.host = credentials.hostName;
@@ -55,7 +55,7 @@ Ikev2Configurator::ConnectionData Ikev2Configurator::prepareIkev2Config(const Se
}
QString Ikev2Configurator::createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig,
ErrorCode errorCode)
ErrorCode &errorCode)
{
Q_UNUSED(containerConfig)

View File

@@ -22,14 +22,14 @@ public:
};
QString createConfig(const ServerCredentials &credentials, DockerContainer container,
const QJsonObject &containerConfig, ErrorCode errorCode);
const QJsonObject &containerConfig, ErrorCode &errorCode);
QString genIkev2Config(const ConnectionData &connData);
QString genMobileConfig(const ConnectionData &connData);
QString genStrongSwanConfig(const ConnectionData &connData);
ConnectionData prepareIkev2Config(const ServerCredentials &credentials,
DockerContainer container, ErrorCode errorCode);
DockerContainer container, ErrorCode &errorCode);
};
#endif // IKEV2_CONFIGURATOR_H

View File

@@ -31,7 +31,7 @@ OpenVpnConfigurator::OpenVpnConfigurator(std::shared_ptr<Settings> settings, con
}
OpenVpnConfigurator::ConnectionData OpenVpnConfigurator::prepareOpenVpnConfig(const ServerCredentials &credentials,
DockerContainer container, ErrorCode errorCode)
DockerContainer container, ErrorCode &errorCode)
{
OpenVpnConfigurator::ConnectionData connData = OpenVpnConfigurator::createCertRequest();
connData.host = credentials.hostName;
@@ -72,7 +72,7 @@ OpenVpnConfigurator::ConnectionData OpenVpnConfigurator::prepareOpenVpnConfig(co
}
QString OpenVpnConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container,
const QJsonObject &containerConfig, ErrorCode errorCode)
const QJsonObject &containerConfig, ErrorCode &errorCode)
{
QString config = m_serverController->replaceVars(amnezia::scriptData(ProtocolScriptType::openvpn_template, container),
m_serverController->genVarsForScript(credentials, container, containerConfig));

View File

@@ -25,7 +25,7 @@ public:
};
QString createConfig(const ServerCredentials &credentials, DockerContainer container,
const QJsonObject &containerConfig, ErrorCode errorCode);
const QJsonObject &containerConfig, ErrorCode &errorCode);
QString processConfigWithLocalSettings(const QPair<QString, QString> &dns, const bool isApiConfig,
QString &protocolConfigString);
@@ -36,7 +36,7 @@ public:
private:
ConnectionData prepareOpenVpnConfig(const ServerCredentials &credentials, DockerContainer container,
ErrorCode errorCode);
ErrorCode &errorCode);
ErrorCode signCert(DockerContainer container, const ServerCredentials &credentials, QString clientId);
};

View File

@@ -14,7 +14,7 @@ ShadowSocksConfigurator::ShadowSocksConfigurator(std::shared_ptr<Settings> setti
}
QString ShadowSocksConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container,
const QJsonObject &containerConfig, ErrorCode errorCode)
const QJsonObject &containerConfig, ErrorCode &errorCode)
{
QString ssKey =
m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::shadowsocks::ssKeyPath, errorCode);

View File

@@ -13,7 +13,7 @@ public:
ShadowSocksConfigurator(std::shared_ptr<Settings> settings, const QSharedPointer<ServerController> &serverController, QObject *parent = nullptr);
QString createConfig(const ServerCredentials &credentials, DockerContainer container,
const QJsonObject &containerConfig, ErrorCode errorCode);
const QJsonObject &containerConfig, ErrorCode &errorCode);
};
#endif // SHADOWSOCKS_CONFIGURATOR_H

View File

@@ -65,7 +65,7 @@ WireguardConfigurator::ConnectionData WireguardConfigurator::genClientKeys()
WireguardConfigurator::ConnectionData WireguardConfigurator::prepareWireguardConfig(const ServerCredentials &credentials,
DockerContainer container,
const QJsonObject &containerConfig, ErrorCode errorCode)
const QJsonObject &containerConfig, ErrorCode &errorCode)
{
WireguardConfigurator::ConnectionData connData = WireguardConfigurator::genClientKeys();
connData.host = credentials.hostName;
@@ -158,7 +158,7 @@ WireguardConfigurator::ConnectionData WireguardConfigurator::prepareWireguardCon
}
QString WireguardConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container,
const QJsonObject &containerConfig, ErrorCode errorCode)
const QJsonObject &containerConfig, ErrorCode &errorCode)
{
QString scriptData = amnezia::scriptData(m_configTemplate, container);
QString config =

View File

@@ -27,7 +27,7 @@ public:
};
QString createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig,
ErrorCode errorCode);
ErrorCode &errorCode);
QString processConfigWithLocalSettings(const QPair<QString, QString> &dns, const bool isApiConfig, QString &protocolConfigString);
QString processConfigWithExportSettings(const QPair<QString, QString> &dns, const bool isApiConfig, QString &protocolConfigString);
@@ -36,7 +36,7 @@ public:
private:
ConnectionData prepareWireguardConfig(const ServerCredentials &credentials, DockerContainer container,
const QJsonObject &containerConfig, ErrorCode errorCode);
const QJsonObject &containerConfig, ErrorCode &errorCode);
bool m_isAwg;
QString m_serverConfigPath;

View File

@@ -14,7 +14,7 @@ XrayConfigurator::XrayConfigurator(std::shared_ptr<Settings> settings, const QSh
}
QString XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig,
ErrorCode errorCode)
ErrorCode &errorCode)
{
QString config = m_serverController->replaceVars(amnezia::scriptData(ProtocolScriptType::xray_template, container),
m_serverController->genVarsForScript(credentials, container, containerConfig));

View File

@@ -13,7 +13,7 @@ public:
XrayConfigurator(std::shared_ptr<Settings> settings, const QSharedPointer<ServerController> &serverController, QObject *parent = nullptr);
QString createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig,
ErrorCode errorCode);
ErrorCode &errorCode);
};
#endif // XRAY_CONFIGURATOR_H

View File

@@ -5,7 +5,9 @@
#include <QNetworkReply>
#include <QtConcurrent>
#include "amnezia_application.h"
#include "configurators/wireguard_configurator.h"
#include "version.h"
namespace
{
@@ -19,7 +21,10 @@ namespace
constexpr char certificate[] = "certificate";
constexpr char publicKey[] = "public_key";
constexpr char protocol[] = "protocol";
constexpr char uuid[] = "installation_uuid";
constexpr char osVersion[] = "os_version";
constexpr char appVersion[] = "app_version";
}
}
@@ -27,8 +32,7 @@ ApiController::ApiController(QObject *parent) : QObject(parent)
{
}
void ApiController::processApiConfig(const QString &protocol, const ApiController::ApiPayloadData &apiPayloadData,
QString &config)
void ApiController::processApiConfig(const QString &protocol, const ApiController::ApiPayloadData &apiPayloadData, QString &config)
{
if (protocol == configKey::cloak) {
config.replace("<key>", "<key>\n");
@@ -61,53 +65,52 @@ QJsonObject ApiController::fillApiPayload(const QString &protocol, const ApiCont
} else if (protocol == configKey::awg) {
obj[configKey::publicKey] = apiPayloadData.wireGuardClientPubKey;
}
obj[configKey::osVersion] = QSysInfo::productType();
obj[configKey::appVersion] = QString(APP_VERSION);
return obj;
}
ErrorCode ApiController::updateServerConfigFromApi(const QString &installationUuid, QJsonObject &serverConfig)
void ApiController::updateServerConfigFromApi(const QString &installationUuid, const int serverIndex, QJsonObject serverConfig)
{
QFutureWatcher<ErrorCode> watcher;
#ifdef Q_OS_IOS
IosController::Instance()->requestInetAccess();
QThread::msleep(10);
#endif
QFuture<ErrorCode> future = QtConcurrent::run([this, &serverConfig, &installationUuid]() {
auto containerConfig = serverConfig.value(config_key::containers).toArray();
auto containerConfig = serverConfig.value(config_key::containers).toArray();
if (serverConfig.value(config_key::configVersion).toInt()) {
QNetworkAccessManager manager;
if (serverConfig.value(config_key::configVersion).toInt()) {
QNetworkRequest request;
request.setTransferTimeout(7000);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Authorization", "Api-Key " + serverConfig.value(configKey::accessToken).toString().toUtf8());
QString endpoint = serverConfig.value(configKey::apiEdnpoint).toString();
request.setUrl(endpoint);
QNetworkRequest request;
request.setTransferTimeout(7000);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Authorization",
"Api-Key " + serverConfig.value(configKey::accessToken).toString().toUtf8());
QString endpoint = serverConfig.value(configKey::apiEdnpoint).toString();
request.setUrl(endpoint);
QString protocol = serverConfig.value(configKey::protocol).toString();
QString protocol = serverConfig.value(configKey::protocol).toString();
ApiPayloadData apiPayloadData = generateApiPayloadData(protocol);
auto apiPayloadData = generateApiPayloadData(protocol);
QJsonObject apiPayload = fillApiPayload(protocol, apiPayloadData);
apiPayload[configKey::uuid] = installationUuid;
auto apiPayload = fillApiPayload(protocol, apiPayloadData);
apiPayload[configKey::uuid] = installationUuid;
QByteArray requestBody = QJsonDocument(apiPayload).toJson();
QByteArray requestBody = QJsonDocument(apiPayload).toJson();
QScopedPointer<QNetworkReply> reply;
reply.reset(manager.post(request, requestBody));
QEventLoop wait;
QObject::connect(reply.get(), &QNetworkReply::finished, &wait, &QEventLoop::quit);
wait.exec();
QNetworkReply *reply = amnApp->manager()->post(request, requestBody); // ??
QObject::connect(reply, &QNetworkReply::finished, [this, reply, protocol, apiPayloadData, serverIndex, serverConfig]() mutable {
if (reply->error() == QNetworkReply::NoError) {
QString contents = QString::fromUtf8(reply->readAll());
auto data = QJsonDocument::fromJson(contents.toUtf8()).object().value(config_key::config).toString();
QString data = QJsonDocument::fromJson(contents.toUtf8()).object().value(config_key::config).toString();
data.replace("vpn://", "");
QByteArray ba = QByteArray::fromBase64(data.toUtf8(),
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
QByteArray ba = QByteArray::fromBase64(data.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
if (ba.isEmpty()) {
return ErrorCode::ApiConfigDownloadError;
emit errorOccurred(ErrorCode::ApiConfigEmptyError);
return;
}
QByteArray ba_uncompressed = qUncompress(ba);
@@ -119,30 +122,37 @@ ErrorCode ApiController::updateServerConfigFromApi(const QString &installationUu
processApiConfig(protocol, apiPayloadData, configStr);
QJsonObject apiConfig = QJsonDocument::fromJson(configStr.toUtf8()).object();
serverConfig.insert(config_key::dns1, apiConfig.value(config_key::dns1));
serverConfig.insert(config_key::dns2, apiConfig.value(config_key::dns2));
serverConfig.insert(config_key::containers, apiConfig.value(config_key::containers));
serverConfig.insert(config_key::hostName, apiConfig.value(config_key::hostName));
serverConfig[config_key::dns1] = apiConfig.value(config_key::dns1);
serverConfig[config_key::dns2] = apiConfig.value(config_key::dns2);
serverConfig[config_key::containers] = apiConfig.value(config_key::containers);
serverConfig[config_key::hostName] = apiConfig.value(config_key::hostName);
auto defaultContainer = apiConfig.value(config_key::defaultContainer).toString();
serverConfig.insert(config_key::defaultContainer, defaultContainer);
serverConfig[config_key::defaultContainer] = defaultContainer;
emit configUpdated(true, serverConfig, serverIndex);
} else {
QString err = reply->errorString();
qDebug() << QString::fromUtf8(reply->readAll());
qDebug() << reply->error();
qDebug() << err;
qDebug() << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
return ErrorCode::ApiConfigDownloadError;
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError
|| reply->error() == QNetworkReply::NetworkError::TimeoutError) {
emit errorOccurred(ErrorCode::ApiConfigTimeoutError);
} else {
QString err = reply->errorString();
qDebug() << QString::fromUtf8(reply->readAll());
qDebug() << reply->error();
qDebug() << err;
qDebug() << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
emit errorOccurred(ErrorCode::ApiConfigDownloadError);
}
}
}
return ErrorCode::NoError;
});
QEventLoop wait;
connect(&watcher, &QFutureWatcher<ErrorCode>::finished, &wait, &QEventLoop::quit);
watcher.setFuture(future);
wait.exec();
reply->deleteLater();
});
return watcher.result();
QObject::connect(reply, &QNetworkReply::errorOccurred,
[this, reply](QNetworkReply::NetworkError error) { qDebug() << reply->errorString() << error; });
connect(reply, &QNetworkReply::sslErrors, [this, reply](const QList<QSslError> &errors) {
qDebug().noquote() << errors;
emit errorOccurred(ErrorCode::ApiConfigSslError);
});
}
}

View File

@@ -5,6 +5,10 @@
#include "configurators/openvpn_configurator.h"
#ifdef Q_OS_IOS
#include "platforms/ios/ios_controller.h"
#endif
class ApiController : public QObject
{
Q_OBJECT
@@ -13,10 +17,15 @@ public:
explicit ApiController(QObject *parent = nullptr);
public slots:
ErrorCode updateServerConfigFromApi(const QString &installationUuid, QJsonObject &serverConfig);
void updateServerConfigFromApi(const QString &installationUuid, const int serverIndex, QJsonObject serverConfig);
signals:
void errorOccurred(ErrorCode errorCode);
void configUpdated(const bool updateConfig, const QJsonObject &config, const int serverIndex);
private:
struct ApiPayloadData {
struct ApiPayloadData
{
OpenVpnConfigurator::ConnectionData certRequest;
QString wireGuardClientPrivKey;

View File

@@ -173,7 +173,7 @@ ErrorCode ServerController::uploadTextFileToContainer(DockerContainer container,
}
QByteArray ServerController::getTextFileFromContainer(DockerContainer container, const ServerCredentials &credentials, const QString &path,
ErrorCode errorCode)
ErrorCode &errorCode)
{
errorCode = ErrorCode::NoError;
@@ -618,7 +618,7 @@ ServerController::Vars ServerController::genVarsForScript(const ServerCredential
return vars;
}
QString ServerController::checkSshConnection(const ServerCredentials &credentials, ErrorCode errorCode)
QString ServerController::checkSshConnection(const ServerCredentials &credentials, ErrorCode &errorCode)
{
QString stdOut;
auto cbReadStdOut = [&](const QString &data, libssh::Client &) {

View File

@@ -36,7 +36,7 @@ public:
const QString &path,
libssh::ScpOverwriteMode overwriteMode = libssh::ScpOverwriteMode::ScpOverwriteExisting);
QByteArray getTextFileFromContainer(DockerContainer container, const ServerCredentials &credentials, const QString &path,
ErrorCode errorCode);
ErrorCode &errorCode);
QString replaceVars(const QString &script, const Vars &vars);
Vars genVarsForScript(const ServerCredentials &credentials, DockerContainer container = DockerContainer::None,
@@ -50,7 +50,7 @@ public:
const std::function<ErrorCode(const QString &, libssh::Client &)> &cbReadStdOut = nullptr,
const std::function<ErrorCode(const QString &, libssh::Client &)> &cbReadStdErr = nullptr);
QString checkSshConnection(const ServerCredentials &credentials, ErrorCode errorCode);
QString checkSshConnection(const ServerCredentials &credentials, ErrorCode &errorCode);
void cancelInstallation();

View File

@@ -77,7 +77,7 @@ ErrorCode VpnConfigurationsController::createProtocolConfigString(const bool isA
QJsonObject VpnConfigurationsController::createVpnConfiguration(const QPair<QString, QString> &dns, const QJsonObject &serverConfig,
const QJsonObject &containerConfig, const DockerContainer container,
ErrorCode errorCode)
ErrorCode &errorCode)
{
QJsonObject vpnConfiguration {};

View File

@@ -21,7 +21,7 @@ public slots:
const DockerContainer container, const QJsonObject &containerConfig, const Proto protocol,
QString &protocolConfigString);
QJsonObject createVpnConfiguration(const QPair<QString, QString> &dns, const QJsonObject &serverConfig,
const QJsonObject &containerConfig, const DockerContainer container, ErrorCode errorCode);
const QJsonObject &containerConfig, const DockerContainer container, ErrorCode &errorCode);
static void updateContainerConfigAfterInstallation(const DockerContainer container, QJsonObject &containerConfig, const QString &stdOut);
signals:

View File

@@ -36,12 +36,17 @@ namespace amnezia
}
};
enum ErrorCode {
namespace error_code_ns
{
Q_NAMESPACE
// TODO: change to enum class
enum ErrorCode {
// General error codes
NoError = 0,
UnknownError = 100,
InternalError = 101,
NotImplementedError = 102,
AmneziaServiceNotRunning = 103,
// Server errors
ServerCheckFailed = 200,
@@ -74,7 +79,7 @@ namespace amnezia
AmneziaServiceConnectionFailed = 603,
ExecutableMissing = 604,
XrayExecutableMissing = 605,
Tun2SockExecutableMissing = 606,
Tun2SockExecutableMissing = 606,
// VPN errors
OpenVpnAdaptersInUseError = 700,
@@ -98,6 +103,9 @@ namespace amnezia
// Api errors
ApiConfigDownloadError = 1100,
ApiConfigAlreadyAdded = 1101,
ApiConfigEmptyError = 1102,
ApiConfigTimeoutError = 1103,
ApiConfigSslError = 1104,
// QFile errors
OpenError = 1200,
@@ -106,7 +114,11 @@ namespace amnezia
UnspecifiedError = 1203,
FatalError = 1204,
AbortError = 1205
};
};
Q_ENUM_NS(ErrorCode)
}
using ErrorCode = error_code_ns::ErrorCode;
} // namespace amnezia

View File

@@ -8,64 +8,68 @@ QString errorString(ErrorCode code) {
switch (code) {
// General error codes
case(NoError): errorMessage = QObject::tr("No error"); break;
case(UnknownError): errorMessage = QObject::tr("Unknown Error"); break;
case(NotImplementedError): errorMessage = QObject::tr("Function not implemented"); break;
case(ErrorCode::NoError): errorMessage = QObject::tr("No error"); break;
case(ErrorCode::UnknownError): errorMessage = QObject::tr("Unknown Error"); break;
case(ErrorCode::NotImplementedError): errorMessage = QObject::tr("Function not implemented"); break;
case(ErrorCode::AmneziaServiceNotRunning): errorMessage = QObject::tr("Background service is not running"); break;
// Server errors
case(ServerCheckFailed): errorMessage = QObject::tr("Server check failed"); break;
case(ServerPortAlreadyAllocatedError): errorMessage = QObject::tr("Server port already used. Check for another software"); break;
case(ServerContainerMissingError): errorMessage = QObject::tr("Server error: Docker container missing"); break;
case(ServerDockerFailedError): errorMessage = QObject::tr("Server error: Docker failed"); break;
case(ServerCancelInstallation): errorMessage = QObject::tr("Installation canceled by user"); break;
case(ServerUserNotInSudo): errorMessage = QObject::tr("The user does not have permission to use sudo"); break;
case(ServerPacketManagerError): errorMessage = QObject::tr("Server error: Packet manager error"); break;
case(ErrorCode::ServerCheckFailed): errorMessage = QObject::tr("Server check failed"); break;
case(ErrorCode::ServerPortAlreadyAllocatedError): errorMessage = QObject::tr("Server port already used. Check for another software"); break;
case(ErrorCode::ServerContainerMissingError): errorMessage = QObject::tr("Server error: Docker container missing"); break;
case(ErrorCode::ServerDockerFailedError): errorMessage = QObject::tr("Server error: Docker failed"); break;
case(ErrorCode::ServerCancelInstallation): errorMessage = QObject::tr("Installation canceled by user"); break;
case(ErrorCode::ServerUserNotInSudo): errorMessage = QObject::tr("The user does not have permission to use sudo"); break;
case(ErrorCode::ServerPacketManagerError): errorMessage = QObject::tr("Server error: Packet manager error"); break;
// Libssh errors
case(SshRequestDeniedError): errorMessage = QObject::tr("Ssh request was denied"); break;
case(SshInterruptedError): errorMessage = QObject::tr("Ssh request was interrupted"); break;
case(SshInternalError): errorMessage = QObject::tr("Ssh internal error"); break;
case(SshPrivateKeyError): errorMessage = QObject::tr("Invalid private key or invalid passphrase entered"); break;
case(SshPrivateKeyFormatError): errorMessage = QObject::tr("The selected private key format is not supported, use openssh ED25519 key types or PEM key types"); break;
case(SshTimeoutError): errorMessage = QObject::tr("Timeout connecting to server"); break;
case(ErrorCode::SshRequestDeniedError): errorMessage = QObject::tr("Ssh request was denied"); break;
case(ErrorCode::SshInterruptedError): errorMessage = QObject::tr("Ssh request was interrupted"); break;
case(ErrorCode::SshInternalError): errorMessage = QObject::tr("Ssh internal error"); break;
case(ErrorCode::SshPrivateKeyError): errorMessage = QObject::tr("Invalid private key or invalid passphrase entered"); break;
case(ErrorCode::SshPrivateKeyFormatError): errorMessage = QObject::tr("The selected private key format is not supported, use openssh ED25519 key types or PEM key types"); break;
case(ErrorCode::SshTimeoutError): errorMessage = QObject::tr("Timeout connecting to server"); break;
// Ssh scp errors
case(SshScpFailureError): errorMessage = QObject::tr("Scp error: Generic failure"); break;
case(ErrorCode::SshScpFailureError): errorMessage = QObject::tr("Scp error: Generic failure"); break;
// Local errors
case (OpenVpnConfigMissing): errorMessage = QObject::tr("OpenVPN config missing"); break;
case (OpenVpnManagementServerError): errorMessage = QObject::tr("OpenVPN management server error"); break;
case (ErrorCode::OpenVpnConfigMissing): errorMessage = QObject::tr("OpenVPN config missing"); break;
case (ErrorCode::OpenVpnManagementServerError): errorMessage = QObject::tr("OpenVPN management server error"); break;
// Distro errors
case (OpenVpnExecutableMissing): errorMessage = QObject::tr("OpenVPN executable missing"); break;
case (ShadowSocksExecutableMissing): errorMessage = QObject::tr("ShadowSocks (ss-local) executable missing"); break;
case (CloakExecutableMissing): errorMessage = QObject::tr("Cloak (ck-client) executable missing"); break;
case (AmneziaServiceConnectionFailed): errorMessage = QObject::tr("Amnezia helper service error"); break;
case (OpenSslFailed): errorMessage = QObject::tr("OpenSSL failed"); break;
case (ErrorCode::OpenVpnExecutableMissing): errorMessage = QObject::tr("OpenVPN executable missing"); break;
case (ErrorCode::ShadowSocksExecutableMissing): errorMessage = QObject::tr("ShadowSocks (ss-local) executable missing"); break;
case (ErrorCode::CloakExecutableMissing): errorMessage = QObject::tr("Cloak (ck-client) executable missing"); break;
case (ErrorCode::AmneziaServiceConnectionFailed): errorMessage = QObject::tr("Amnezia helper service error"); break;
case (ErrorCode::OpenSslFailed): errorMessage = QObject::tr("OpenSSL failed"); break;
// VPN errors
case (OpenVpnAdaptersInUseError): errorMessage = QObject::tr("Can't connect: another VPN connection is active"); break;
case (OpenVpnTapAdapterError): errorMessage = QObject::tr("Can't setup OpenVPN TAP network adapter"); break;
case (AddressPoolError): errorMessage = QObject::tr("VPN pool error: no available addresses"); break;
case (ErrorCode::OpenVpnAdaptersInUseError): errorMessage = QObject::tr("Can't connect: another VPN connection is active"); break;
case (ErrorCode::OpenVpnTapAdapterError): errorMessage = QObject::tr("Can't setup OpenVPN TAP network adapter"); break;
case (ErrorCode::AddressPoolError): errorMessage = QObject::tr("VPN pool error: no available addresses"); break;
case (ImportInvalidConfigError): errorMessage = QObject::tr("The config does not contain any containers and credentials for connecting to the server"); break;
case (ErrorCode::ImportInvalidConfigError): errorMessage = QObject::tr("The config does not contain any containers and credentials for connecting to the server"); break;
// Android errors
case (AndroidError): errorMessage = QObject::tr("VPN connection error"); break;
case (ErrorCode::AndroidError): errorMessage = QObject::tr("VPN connection error"); break;
// Api errors
case (ApiConfigDownloadError): errorMessage = QObject::tr("Error when retrieving configuration from API"); break;
case (ApiConfigAlreadyAdded): errorMessage = QObject::tr("This config has already been added to the application"); break;
case (ErrorCode::ApiConfigDownloadError): errorMessage = QObject::tr("Error when retrieving configuration from API"); break;
case (ErrorCode::ApiConfigAlreadyAdded): errorMessage = QObject::tr("This config has already been added to the application"); break;
case (ErrorCode::ApiConfigEmptyError): errorMessage = QObject::tr("In the response from the server, an empty config was received"); break;
case (ErrorCode::ApiConfigSslError): errorMessage = QObject::tr("SSL error occurred"); break;
case (ErrorCode::ApiConfigTimeoutError): errorMessage = QObject::tr("Server response timeout on api request"); break;
// QFile errors
case(OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
case(ReadError): errorMessage = QObject::tr("QFile error: An error occurred when reading from the file"); break;
case(PermissionsError): errorMessage = QObject::tr("QFile error: The file could not be accessed"); break;
case(UnspecifiedError): errorMessage = QObject::tr("QFile error: An unspecified error occurred"); break;
case(FatalError): errorMessage = QObject::tr("QFile error: A fatal error occurred"); break;
case(AbortError): errorMessage = QObject::tr("QFile error: The operation was aborted"); break;
case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
case(ErrorCode::ReadError): errorMessage = QObject::tr("QFile error: An error occurred when reading from the file"); break;
case(ErrorCode::PermissionsError): errorMessage = QObject::tr("QFile error: The file could not be accessed"); break;
case(ErrorCode::UnspecifiedError): errorMessage = QObject::tr("QFile error: An unspecified error occurred"); break;
case(ErrorCode::FatalError): errorMessage = QObject::tr("QFile error: A fatal error occurred"); break;
case(ErrorCode::AbortError): errorMessage = QObject::tr("QFile error: The operation was aborted"); break;
case(InternalError):
case(ErrorCode::InternalError):
default:
errorMessage = QObject::tr("Internal error"); break;
}

View File

@@ -51,6 +51,13 @@
<true/>
<key>NSCameraUsageDescription</key>
<string>Amnezia VPN needs access to the camera for reading QR-codes.</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>CFBundleIcons</key>
<dict/>
<key>CFBundleIcons~ipad</key>

View File

@@ -64,6 +64,7 @@ int main(int argc, char *argv[])
qInfo().noquote() << QString("Started %1 version %2 %3").arg(APPLICATION_NAME, APP_VERSION, GIT_COMMIT_HASH);
qInfo().noquote() << QString("%1 (%2)").arg(QSysInfo::prettyProductName(), QSysInfo::currentCpuArchitecture());
qInfo().noquote() << QString("SSL backend: %1").arg(QSslSocket::sslLibraryVersionString());
return app.exec();
}

View File

@@ -171,9 +171,15 @@ void NetworkWatcher::unsecuredNetwork(const QString& networkName,
}
QString NetworkWatcher::getCurrentTransport() {
auto type = m_impl->getTransportType();
QMetaEnum metaEnum = QMetaEnum::fromType<NetworkWatcherImpl::TransportType>();
return QString(metaEnum.valueToKey(type))
.remove("TransportType_", Qt::CaseSensitive);
QNetworkInformation::Reachability NetworkWatcher::getReachability() {
if (m_simulatedDisconnection) {
return QNetworkInformation::Reachability::Disconnected;
} else if (QNetworkInformation::instance()) {
return QNetworkInformation::instance()->reachability();
}
return QNetworkInformation::Reachability::Unknown;
}
void NetworkWatcher::simulateDisconnection(bool simulatedDisconnection) {
m_simulatedDisconnection = simulatedDisconnection;
}

View File

@@ -7,45 +7,50 @@
#include <QElapsedTimer>
#include <QMap>
#include <QObject>
#include <QNetworkInformation>
class NetworkWatcherImpl;
// This class watches for network changes to detect unsecured wifi.
class NetworkWatcher final : public QObject {
Q_OBJECT
Q_DISABLE_COPY_MOVE(NetworkWatcher)
Q_OBJECT
Q_DISABLE_COPY_MOVE(NetworkWatcher)
public:
NetworkWatcher();
~NetworkWatcher();
public:
NetworkWatcher();
~NetworkWatcher();
void initialize();
void initialize();
// public for the inspector.
void unsecuredNetwork(const QString& networkName, const QString& networkId);
// Public for the Inspector.
void unsecuredNetwork(const QString& networkName, const QString& networkId);
// Used for the Inspector. simulateOffline = true to mock being disconnected,
// false to restore.
void simulateDisconnection(bool simulatedDisconnection);
QString getCurrentTransport();
QNetworkInformation::Reachability getReachability();
signals:
void networkChange();
signals:
void networkChange();
private:
void settingsChanged();
private:
void settingsChanged();
// void notificationClicked(NotificationHandler::Message message);
private:
bool m_active = false;
bool m_reportUnsecuredNetwork = false;
private:
bool m_active = false;
bool m_reportUnsecuredNetwork = false;
// Platform-specific implementation.
NetworkWatcherImpl* m_impl = nullptr;
// Platform-specific implementation.
NetworkWatcherImpl* m_impl = nullptr;
QMap<QString, QElapsedTimer> m_networks;
QMap<QString, QElapsedTimer> m_networks;
// This is used to connect NotificationHandler lazily.
bool m_firstNotification = true;
// This is used to connect NotificationHandler lazily.
bool m_firstNotification = true;
// Used to simulate network disconnection in the Inspector
bool m_simulatedDisconnection = false;
};
#endif // NETWORKWATCHER_H

View File

@@ -5,50 +5,45 @@
#ifndef NETWORKWATCHERIMPL_H
#define NETWORKWATCHERIMPL_H
#include <QNetworkInformation>
#include <QObject>
class NetworkWatcherImpl : public QObject {
Q_OBJECT
Q_DISABLE_COPY_MOVE(NetworkWatcherImpl)
Q_OBJECT
Q_DISABLE_COPY_MOVE(NetworkWatcherImpl)
public:
NetworkWatcherImpl(QObject* parent) : QObject(parent) {}
public:
NetworkWatcherImpl(QObject* parent) : QObject(parent) {}
virtual ~NetworkWatcherImpl() = default;
virtual ~NetworkWatcherImpl() = default;
virtual void initialize() = 0;
virtual void initialize() = 0;
virtual void start() { m_active = true; }
virtual void stop() { m_active = false; }
virtual void start() { m_active = true; }
virtual void stop() { m_active = false; }
bool isActive() const { return m_active; }
bool isActive() const { return m_active; }
enum TransportType {
TransportType_Unknown = 0,
TransportType_Ethernet = 1,
TransportType_WiFi = 2,
TransportType_Cellular = 3, // In Case the API does not retun the gsm type
TransportType_Other = 4, // I.e USB thethering
TransportType_None = 5 // I.e Airplane Mode or no active network device
};
Q_ENUM(TransportType);
enum TransportType {
TransportType_Unknown = 0,
TransportType_Ethernet = 1,
TransportType_WiFi = 2,
TransportType_Cellular = 3, // In Case the API does not retun the gsm type
TransportType_Other = 4, // I.e USB thethering
TransportType_None = 5 // I.e Airplane Mode or no active network device
};
Q_ENUM(TransportType);
// Returns the current type of Network Connection
virtual TransportType getTransportType() = 0;
signals:
// Fires when the Device Connects to an unsecured Network
void unsecuredNetwork(const QString& networkName, const QString& networkId);
// Fires on when the connected WIFI Changes
// TODO: Only windows-networkwatcher has this, the other plattforms should
// too.
void networkChanged(QString newBSSID);
signals:
// Fires when the Device Connects to an unsecured Network
void unsecuredNetwork(const QString& networkName, const QString& networkId);
// Fires on when the connected WIFI Changes
// TODO: Only windows-networkwatcher has this, the other plattforms should
// too.
void networkChanged(QString newBSSID);
// Fired when the Device changed the Type of Transport
void transportChanged(NetworkWatcherImpl::TransportType transportType);
private:
bool m_active = false;
private:
bool m_active = false;
};
#endif // NETWORKWATCHERIMPL_H

View File

@@ -93,6 +93,7 @@ bool AndroidController::initialize()
{"onServiceDisconnected", "()V", reinterpret_cast<void *>(onServiceDisconnected)},
{"onServiceError", "()V", reinterpret_cast<void *>(onServiceError)},
{"onVpnPermissionRejected", "()V", reinterpret_cast<void *>(onVpnPermissionRejected)},
{"onNotificationStateChanged", "()V", reinterpret_cast<void *>(onNotificationStateChanged)},
{"onVpnStateChanged", "(I)V", reinterpret_cast<void *>(onVpnStateChanged)},
{"onStatisticsUpdate", "(JJ)V", reinterpret_cast<void *>(onStatisticsUpdate)},
{"onFileOpened", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onFileOpened)},
@@ -135,7 +136,7 @@ ErrorCode AndroidController::start(const QJsonObject &vpnConfig)
callActivityMethod("start", "(Ljava/lang/String;)V",
QJniObject::fromString(config).object<jstring>());
return NoError;
return ErrorCode::NoError;
}
void AndroidController::stop()
@@ -173,14 +174,6 @@ QString AndroidController::openFile(const QString &filter)
return fileName;
}
void AndroidController::setNotificationText(const QString &title, const QString &message, int timerSec)
{
callActivityMethod("setNotificationText", "(Ljava/lang/String;Ljava/lang/String;I)V",
QJniObject::fromString(title).object<jstring>(),
QJniObject::fromString(message).object<jstring>(),
(jint) timerSec);
}
bool AndroidController::isCameraPresent()
{
return callActivityMethod<jboolean>("isCameraPresent", "()Z");
@@ -257,6 +250,16 @@ QPixmap AndroidController::getAppIcon(const QString &package, QSize *size, const
return QPixmap::fromImage(image);
}
bool AndroidController::isNotificationPermissionGranted()
{
return callActivityMethod<jboolean>("isNotificationPermissionGranted", "()Z");
}
void AndroidController::requestNotificationPermission()
{
callActivityMethod("requestNotificationPermission", "()V");
}
// Moving log processing to the Android side
jclass AndroidController::log;
jmethodID AndroidController::logDebug;
@@ -409,6 +412,15 @@ void AndroidController::onVpnPermissionRejected(JNIEnv *env, jobject thiz)
emit AndroidController::instance()->vpnPermissionRejected();
}
// static
void AndroidController::onNotificationStateChanged(JNIEnv *env, jobject thiz)
{
Q_UNUSED(env);
Q_UNUSED(thiz);
emit AndroidController::instance()->notificationStateChanged();
}
// static
void AndroidController::onVpnStateChanged(JNIEnv *env, jobject thiz, jint stateCode)
{

View File

@@ -32,7 +32,6 @@ public:
ErrorCode start(const QJsonObject &vpnConfig);
void stop();
void resetLastServer(int serverIndex);
void setNotificationText(const QString &title, const QString &message, int timerSec);
void saveFile(const QString &fileName, const QString &data);
QString openFile(const QString &filter);
bool isCameraPresent();
@@ -44,6 +43,8 @@ public:
void minimizeApp();
QJsonArray getAppList();
QPixmap getAppIcon(const QString &package, QSize *size, const QSize &requestedSize);
bool isNotificationPermissionGranted();
void requestNotificationPermission();
static bool initLogging();
static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message);
@@ -54,6 +55,7 @@ signals:
void serviceDisconnected();
void serviceError();
void vpnPermissionRejected();
void notificationStateChanged();
void vpnStateChanged(ConnectionState state);
void statisticsUpdated(quint64 rxBytes, quint64 txBytes);
void fileOpened(QString uri);
@@ -81,6 +83,7 @@ private:
static void onServiceDisconnected(JNIEnv *env, jobject thiz);
static void onServiceError(JNIEnv *env, jobject thiz);
static void onVpnPermissionRejected(JNIEnv *env, jobject thiz);
static void onNotificationStateChanged(JNIEnv *env, jobject thiz);
static void onVpnStateChanged(JNIEnv *env, jobject thiz, jint stateCode);
static void onStatisticsUpdate(JNIEnv *env, jobject thiz, jlong rxBytes, jlong txBytes);
static void onConfigImported(JNIEnv *env, jobject thiz, jstring data);

View File

@@ -1,21 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "android_notificationhandler.h"
#include "platforms/android/android_controller.h"
AndroidNotificationHandler::AndroidNotificationHandler(QObject* parent)
: NotificationHandler(parent) {
}
AndroidNotificationHandler::~AndroidNotificationHandler() {
}
void AndroidNotificationHandler::notify(NotificationHandler::Message type,
const QString& title,
const QString& message, int timerMsec) {
Q_UNUSED(type);
qDebug() << "Send notification - " << message;
AndroidController::instance()->setNotificationText(title, message,
timerMsec / 1000);
}

View File

@@ -1,24 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef ANDROIDNOTIFICATIONHANDLER_H
#define ANDROIDNOTIFICATIONHANDLER_H
#include "ui/notificationhandler.h"
#include <QObject>
class AndroidNotificationHandler final : public NotificationHandler {
Q_DISABLE_COPY_MOVE(AndroidNotificationHandler)
public:
AndroidNotificationHandler(QObject* parent);
~AndroidNotificationHandler();
protected:
void notify(Message type, const QString& title, const QString& message,
int timerMsec) override;
};
#endif // ANDROIDNOTIFICATIONHANDLER_H

View File

@@ -8,11 +8,11 @@
DummyNetworkWatcher::DummyNetworkWatcher(QObject* parent)
: NetworkWatcherImpl(parent) {
MZ_COUNT_CTOR(DummyNetworkWatcher);
MZ_COUNT_CTOR(DummyNetworkWatcher);
}
DummyNetworkWatcher::~DummyNetworkWatcher() {
MZ_COUNT_DTOR(DummyNetworkWatcher);
MZ_COUNT_DTOR(DummyNetworkWatcher);
}
void DummyNetworkWatcher::initialize() {}

View File

@@ -8,15 +8,11 @@
#include "networkwatcherimpl.h"
class DummyNetworkWatcher final : public NetworkWatcherImpl {
public:
DummyNetworkWatcher(QObject* parent);
~DummyNetworkWatcher();
public:
DummyNetworkWatcher(QObject* parent);
~DummyNetworkWatcher();
void initialize() override;
NetworkWatcherImpl::TransportType getTransportType() override {
return TransportType_Other;
};
void initialize() override;
};
#endif // DUMMYNETWORKWATCHER_H

View File

@@ -1,15 +0,0 @@
#include "MobileUtils.h"
MobileUtils::MobileUtils(QObject *parent) : QObject(parent)
{
}
bool MobileUtils::shareText(const QStringList &)
{
return false;
}
QString MobileUtils::openFile()
{
return QString();
}

View File

@@ -1,22 +0,0 @@
#ifndef MOBILEUTILS_H
#define MOBILEUTILS_H
#include <QObject>
#include <QStringList>
class MobileUtils : public QObject
{
Q_OBJECT
public:
explicit MobileUtils(QObject *parent = nullptr);
public slots:
bool shareText(const QStringList &filesToSend);
QString openFile();
signals:
void finished();
};
#endif // MOBILEUTILS_H

View File

@@ -1,109 +0,0 @@
#include "MobileUtils.h"
#include <UIKit/UIKit.h>
#include <Security/Security.h>
#include <QEventLoop>
static UIViewController* getViewController() {
NSArray *windows = [[UIApplication sharedApplication]windows];
for (UIWindow *window in windows) {
if (window.isKeyWindow) {
return window.rootViewController;
}
}
return nil;
}
MobileUtils::MobileUtils(QObject *parent) : QObject(parent) {
}
bool MobileUtils::shareText(const QStringList& filesToSend) {
NSMutableArray *sharingItems = [NSMutableArray new];
for (int i = 0; i < filesToSend.size(); i++) {
NSURL *logFileUrl = [[NSURL alloc] initFileURLWithPath:filesToSend[i].toNSString()];
[sharingItems addObject:logFileUrl];
}
UIViewController *qtController = getViewController();
if (!qtController) return;
UIActivityViewController *activityController = [[UIActivityViewController alloc] initWithActivityItems:sharingItems applicationActivities:nil];
__block bool isAccepted = false;
[activityController setCompletionWithItemsHandler:^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) {
isAccepted = completed;
emit finished();
}];
[qtController presentViewController:activityController animated:YES completion:nil];
UIPopoverPresentationController *popController = activityController.popoverPresentationController;
if (popController) {
popController.sourceView = qtController.view;
popController.sourceRect = CGRectMake(100, 100, 100, 100);
}
QEventLoop wait;
QObject::connect(this, &MobileUtils::finished, &wait, &QEventLoop::quit);
wait.exec();
return isAccepted;
}
typedef void (^DocumentPickerClosedCallback)(NSString *path);
@interface DocumentPickerDelegate : NSObject <UIDocumentPickerDelegate>
@property (nonatomic, copy) DocumentPickerClosedCallback documentPickerClosedCallback;
@end
@implementation DocumentPickerDelegate
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls {
for (NSURL *url in urls) {
if (self.documentPickerClosedCallback) {
self.documentPickerClosedCallback([url path]);
}
}
}
- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller {
if (self.documentPickerClosedCallback) {
self.documentPickerClosedCallback(nil);
}
}
@end
QString MobileUtils::openFile() {
UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[@"public.item"] inMode:UIDocumentPickerModeOpen];
DocumentPickerDelegate *documentPickerDelegate = [[DocumentPickerDelegate alloc] init];
documentPicker.delegate = documentPickerDelegate;
UIViewController *qtController = getViewController();
if (!qtController) return;
[qtController presentViewController:documentPicker animated:YES completion:nil];
__block QString filePath;
documentPickerDelegate.documentPickerClosedCallback = ^(NSString *path) {
if (path) {
filePath = QString::fromUtf8(path.UTF8String);
} else {
filePath = QString();
}
emit finished();
};
QEventLoop wait;
QObject::connect(this, &MobileUtils::finished, &wait, &QEventLoop::quit);
wait.exec();
return filePath;
}

View File

@@ -50,12 +50,19 @@ public:
void getBackendLogs(std::function<void(const QString &)> &&callback);
void checkStatus();
bool shareText(const QStringList &filesToSend);
QString openFile();
void requestInetAccess();
signals:
void connectionStateChanged(Vpn::ConnectionState state);
void bytesChanged(quint64 receivedBytes, quint64 sentBytes);
void importConfigFromOutside(const QString);
void importBackupFromOutside(const QString);
void finished();
protected slots:
private:

View File

@@ -6,6 +6,7 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QThread>
#include <QEventLoop>
#include "../protocols/vpnprotocol.h"
#import "ios_controller_wrapper.h"
@@ -26,6 +27,15 @@ const char* MessageKey::isOnDemand = "is-on-demand";
const char* MessageKey::SplitTunnelType = "SplitTunnelType";
const char* MessageKey::SplitTunnelSites = "SplitTunnelSites";
static UIViewController* getViewController() {
NSArray *windows = [[UIApplication sharedApplication]windows];
for (UIWindow *window in windows) {
if (window.isKeyWindow) {
return window.rootViewController;
}
}
return nil;
}
Vpn::ConnectionState iosStatusToState(NEVPNStatus status) {
switch (status) {
@@ -703,3 +713,86 @@ void IosController::sendVpnExtensionMessage(NSDictionary* message, std::function
}
}
bool IosController::shareText(const QStringList& filesToSend) {
NSMutableArray *sharingItems = [NSMutableArray new];
for (int i = 0; i < filesToSend.size(); i++) {
NSURL *logFileUrl = [[NSURL alloc] initFileURLWithPath:filesToSend[i].toNSString()];
[sharingItems addObject:logFileUrl];
}
UIViewController *qtController = getViewController();
if (!qtController) return;
UIActivityViewController *activityController = [[UIActivityViewController alloc] initWithActivityItems:sharingItems applicationActivities:nil];
__block bool isAccepted = false;
[activityController setCompletionWithItemsHandler:^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) {
isAccepted = completed;
emit finished();
}];
[qtController presentViewController:activityController animated:YES completion:nil];
UIPopoverPresentationController *popController = activityController.popoverPresentationController;
if (popController) {
popController.sourceView = qtController.view;
popController.sourceRect = CGRectMake(100, 100, 100, 100);
}
QEventLoop wait;
QObject::connect(this, &IosController::finished, &wait, &QEventLoop::quit);
wait.exec();
return isAccepted;
}
QString IosController::openFile() {
UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[@"public.item"] inMode:UIDocumentPickerModeOpen];
DocumentPickerDelegate *documentPickerDelegate = [[DocumentPickerDelegate alloc] init];
documentPicker.delegate = documentPickerDelegate;
UIViewController *qtController = getViewController();
if (!qtController) return;
[qtController presentViewController:documentPicker animated:YES completion:nil];
__block QString filePath;
documentPickerDelegate.documentPickerClosedCallback = ^(NSString *path) {
if (path) {
filePath = QString::fromUtf8(path.UTF8String);
} else {
filePath = QString();
}
emit finished();
};
QEventLoop wait;
QObject::connect(this, &IosController::finished, &wait, &QEventLoop::quit);
wait.exec();
return filePath;
}
void IosController::requestInetAccess() {
NSURL *url = [NSURL URLWithString:@"http://captive.apple.com/generate_204"];
if (url) {
qDebug() << "IosController::requestInetAccess URL error";
return;
}
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) {
qDebug() << "IosController::requestInetAccess error:" << error.localizedDescription;
} else {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
QString responseBody = QString::fromUtf8((const char*)data.bytes, data.length);
qDebug() << "IosController::requestInetAccess server response:" << httpResponse.statusCode << "\n\n" <<responseBody;
}
}];
[task resume];
}

View File

@@ -1,6 +1,8 @@
#import <NetworkExtension/NetworkExtension.h>
#import <NetworkExtension/NETunnelProviderSession.h>
#import <Foundation/Foundation.h>
#include <UIKit/UIKit.h>
#include <Security/Security.h>
class IosController;
@@ -13,3 +15,11 @@ class IosController;
- (void)vpnConfigurationDidChange:(NSNotification *)notification;
@end
typedef void (^DocumentPickerClosedCallback)(NSString *path);
@interface DocumentPickerDelegate : NSObject <UIDocumentPickerDelegate>
@property (nonatomic, copy) DocumentPickerClosedCallback documentPickerClosedCallback;
@end

View File

@@ -24,5 +24,22 @@
// cppController->vpnStatusDidChange(notification);
}
@end
@implementation DocumentPickerDelegate
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls {
for (NSURL *url in urls) {
if (self.documentPickerClosedCallback) {
self.documentPickerClosedCallback([url path]);
}
}
}
- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller {
if (self.documentPickerClosedCallback) {
self.documentPickerClosedCallback(nil);
}
}
@end

View File

@@ -15,7 +15,6 @@ class IOSNetworkWatcher : public NetworkWatcherImpl {
~IOSNetworkWatcher();
void initialize() override;
NetworkWatcherImpl::TransportType getTransportType() override;
private:
NetworkWatcherImpl::TransportType toTransportType(nw_path_t path);

View File

@@ -37,16 +37,6 @@ void IOSNetworkWatcher::initialize() {
//TODO IMPL FOR AMNEZIA
}
NetworkWatcherImpl::TransportType IOSNetworkWatcher::getTransportType() {
//TODO IMPL FOR AMNEZIA
if (m_observableConnection != nil) {
return m_currentVPNTransport;
}
// If we don't have an open tunnel-observer, m_currentVPNTransport is probably wrong.
return NetworkWatcherImpl::TransportType_Unknown;
}
NetworkWatcherImpl::TransportType IOSNetworkWatcher::toTransportType(nw_path_t path) {
if (path == nil) {
return NetworkWatcherImpl::TransportType_Unknown;

5
client/platforms/linux/linuxnetworkwatcher.h Normal file → Executable file
View File

@@ -22,11 +22,6 @@ class LinuxNetworkWatcher final : public NetworkWatcherImpl {
void start() override;
NetworkWatcherImpl::TransportType getTransportType() {
// TODO: Find out how to do that on linux generally. (VPN-2382)
return NetworkWatcherImpl::TransportType_Unknown;
};
signals:
void checkDevicesInThread();

View File

@@ -95,6 +95,11 @@ void MacosRouteMonitor::handleRtmDelete(const struct rt_msghdr* rtm,
!(rtm->rtm_addrs & RTA_NETMASK) || (addrlist.count() < 3)) {
return;
}
// Ignore interface-scoped routes, we want to find the default route to the
// internet in the global scope.
if (rtm->rtm_flags & RTF_IFSCOPE) {
return;
}
// Check for a default route, which should have a netmask of zero.
const struct sockaddr* sa =
@@ -156,6 +161,11 @@ void MacosRouteMonitor::handleRtmUpdate(const struct rt_msghdr* rtm,
!(rtm->rtm_addrs & RTA_NETMASK) || (addrlist.count() < 3)) {
return;
}
// Ignore interface-scoped routes, we want to find the default route to the
// internet in the global scope.
if (rtm->rtm_flags & RTF_IFSCOPE) {
return;
}
// Ignore route changes that we caused, or routes on the tunnel interface.
if (rtm->rtm_index == m_ifindex) {
return;

View File

@@ -83,7 +83,7 @@ void MacOSPingSender::sendPing(const QHostAddress& dest, quint16 sequence) {
packet.icmp_seq = htons(sequence);
packet.icmp_cksum = inetChecksum(&packet, sizeof(packet));
if (sendto(m_socket, (char*)&packet, sizeof(packet), 0,
if (sendto(m_socket, (char*)&packet, sizeof(packet), MSG_NOSIGNAL,
(struct sockaddr*)&addr, sizeof(addr)) != sizeof(packet)) {
logger.error() << "ping sending failed:" << strerror(errno);
emit criticalPingError();
@@ -107,9 +107,9 @@ void MacOSPingSender::socketReady() {
iov.iov_base = packet;
iov.iov_len = IP_MAXPACKET;
ssize_t rc = recvmsg(m_socket, &msg, MSG_DONTWAIT);
ssize_t rc = recvmsg(m_socket, &msg, MSG_DONTWAIT | MSG_NOSIGNAL);
if (rc <= 0) {
logger.error() << "Recvmsg failed";
logger.error() << "Recvmsg failed:" << strerror(errno);
return;
}

View File

@@ -41,11 +41,33 @@ void MacOSUtils::enableLoginItem(bool startAtBoot) {
Q_ASSERT(appId);
NSString* loginItemAppId =
QString("%1.login-item").arg(QString::fromNSString(appId)).toNSString();
CFStringRef cfs = (__bridge CFStringRef)loginItemAppId;
QString("%1.login-item").arg(QString::fromNSString(appId)).toNSString();
Boolean ok = SMLoginItemSetEnabled(cfs, startAtBoot ? YES : NO);
logger.debug() << "Result: " << ok;
// For macOS 13 and beyond, register() and unregister() methods
// are used for managing login items since SMLoginItemSetEnabled() is deprecated.
// For versions prior to macOS 13, SMLoginItemSetEnabled() is used.
if (@available(macOS 13, *)) {
// Use register() or unregister() based on the startAtBoot flag
NSError* error = nil;
if (startAtBoot) {
if (![[SMAppService mainAppService] registerAndReturnError: & error]) {
logger.error() << "Failed to register Amnezia VPN LoginItem: " << error.localizedDescription;
} else {
logger.debug() << "Amnezia VPN LoginItem registered successfully.";
}
} else {
if (![[SMAppService mainAppService] unregisterAndReturnError: & error]) {
logger.error() << "Failed to unregister Amnezia VPN LoginItem: " << error.localizedDescription;
} else {
logger.debug() << "LoginItem unregistered successfully.";
}
}
} else {
CFStringRef cfs = (__bridge CFStringRef) loginItemAppId;
Boolean ok = SMLoginItemSetEnabled(cfs, startAtBoot ? YES : NO);
logger.debug() << "Result: " << ok;
}
}
namespace {

View File

@@ -4,8 +4,11 @@
#include "dnsutilswindows.h"
#include <WS2tcpip.h>
#include <iphlpapi.h>
#include <windows.h>
#include <winsock2.h>
#include <ws2ipdef.h>
#include <QProcess>
#include <QTextStream>
@@ -39,30 +42,27 @@ DnsUtilsWindows::~DnsUtilsWindows() {
bool DnsUtilsWindows::updateResolvers(const QString& ifname,
const QList<QHostAddress>& resolvers) {
NET_LUID luid;
if (ConvertInterfaceAliasToLuid((wchar_t*)ifname.utf16(), &luid) != 0) {
MIB_IF_ROW2 entry;
if (ConvertInterfaceAliasToLuid((wchar_t*)ifname.utf16(),
&entry.InterfaceLuid) != 0) {
logger.error() << "Failed to resolve LUID for" << ifname;
return false;
}
m_luid = luid.Value;
if (GetIfEntry2(&entry) != NO_ERROR) {
logger.error() << "Failed to resolve interface for" << ifname;
return false;
}
m_luid = entry.InterfaceLuid.Value;
logger.debug() << "Configuring DNS for" << ifname;
if (m_setInterfaceDnsSettingsProcAddr == nullptr) {
return updateResolversNetsh(resolvers);
return updateResolversNetsh(entry.InterfaceIndex, resolvers);
}
return updateResolversWin32(resolvers);
return updateResolversWin32(entry.InterfaceGuid, resolvers);
}
bool DnsUtilsWindows::updateResolversWin32(
const QList<QHostAddress>& resolvers) {
GUID guid;
NET_LUID luid;
luid.Value = m_luid;
if (ConvertInterfaceLuidToGuid(&luid, &guid) != NO_ERROR) {
logger.error() << "Failed to resolve GUID";
return false;
}
GUID guid, const QList<QHostAddress>& resolvers) {
QStringList v4resolvers;
QStringList v6resolvers;
for (const QHostAddress& addr : resolvers) {
@@ -113,16 +113,8 @@ constexpr const char* netshAddTemplate =
"interface %1 add dnsservers name=%2 address=%3 validate=no\r\n";
bool DnsUtilsWindows::updateResolversNetsh(
const QList<QHostAddress>& resolvers) {
int ifindex, const QList<QHostAddress>& resolvers) {
QProcess netsh;
NET_LUID luid;
NET_IFINDEX ifindex;
luid.Value = m_luid;
if (ConvertInterfaceLuidToIndex(&luid, &ifindex) != NO_ERROR) {
logger.error() << "Failed to resolve GUID";
return false;
}
netsh.setProgram("netsh");
netsh.start();
if (!netsh.waitForStarted(WINDOWS_NETSH_TIMEOUT_MSEC)) {
@@ -166,12 +158,26 @@ bool DnsUtilsWindows::updateResolversNetsh(
bool DnsUtilsWindows::restoreResolvers() {
if (m_luid == 0) {
// If the DNS hasn't been configured, there is nothing to restore.
return true;
}
MIB_IF_ROW2 entry;
DWORD error;
entry.InterfaceLuid.Value = m_luid;
error = GetIfEntry2(&entry);
if (error == ERROR_FILE_NOT_FOUND) {
// If the interface no longer exists, there is nothing to restore.
return true;
}
if (error != NO_ERROR) {
logger.error() << "Failed to resolve interface entry:" << error;
return false;
}
QList<QHostAddress> empty;
if (m_setInterfaceDnsSettingsProcAddr == nullptr) {
return updateResolversNetsh(empty);
return updateResolversNetsh(entry.InterfaceIndex, empty);
}
return updateResolversWin32(empty);
return updateResolversWin32(entry.InterfaceGuid, empty);
}

View File

@@ -27,8 +27,8 @@ class DnsUtilsWindows final : public DnsUtils {
quint64 m_luid = 0;
DWORD (*m_setInterfaceDnsSettingsProcAddr)(GUID, const void*) = nullptr;
bool updateResolversWin32(const QList<QHostAddress>& resolvers);
bool updateResolversNetsh(const QList<QHostAddress>& resolvers);
bool updateResolversWin32(GUID, const QList<QHostAddress>& resolvers);
bool updateResolversNetsh(int ifindex, const QList<QHostAddress>& resolvers);
};
#endif // DNSUTILSWINDOWS_H

View File

@@ -38,7 +38,6 @@ class WindowsDaemon final : public Daemon {
Inactive,
};
State m_state = Inactive;
int m_inetAdapterIndex = -1;
WireguardUtilsWindows* m_wgutils = nullptr;

View File

@@ -114,7 +114,7 @@ void WindowsRouteMonitor::updateValidInterfaces(int family) {
void WindowsRouteMonitor::updateExclusionRoute(MIB_IPFORWARD_ROW2* data,
void* ptable) {
PMIB_IPFORWARD_TABLE2 table = reinterpret_cast<PMIB_IPFORWARD_TABLE2>(ptable);
SOCKADDR_INET nexthop = {0};
SOCKADDR_INET nexthop = {};
quint64 bestLuid = 0;
int bestMatch = -1;
ULONG bestMetric = ULONG_MAX;

View File

@@ -39,7 +39,7 @@ Logger logger("tunnel.dll");
WindowsTunnelLogger::WindowsTunnelLogger(const QString& filename,
QObject* parent)
: QObject(parent), m_logfile(filename, this), m_timer(this) {
: QObject(parent), m_timer(this), m_logfile(filename, this) {
MZ_COUNT_CTOR(WindowsTunnelLogger);
m_startTime = QDateTime::currentMSecsSinceEpoch() * 1000000;

View File

@@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "WindowsTunnelService.h"
#include "windowstunnelservice.h"
#include <Windows.h>
@@ -30,12 +30,22 @@ static bool waitForServiceStatus(SC_HANDLE service, DWORD expectedStatus);
WindowsTunnelService::WindowsTunnelService(QObject* parent) : QObject(parent) {
MZ_COUNT_CTOR(WindowsTunnelService);
logger.debug() << "WindowsTunnelService created.";
m_scm = OpenSCManager(nullptr, nullptr, SC_MANAGER_ALL_ACCESS);
if (m_scm == nullptr) {
WindowsUtils::windowsLog("Failed to open SCManager");
}
// Is the service already running? Terminate it.
SC_HANDLE service =
OpenService((SC_HANDLE)m_scm, TUNNEL_SERVICE_NAME, SERVICE_ALL_ACCESS);
if (service != nullptr) {
logger.info() << "Tunnel already exists. Terminating it.";
stopAndDeleteTunnelService(service);
CloseServiceHandle(service);
}
connect(&m_timer, &QTimer::timeout, this, &WindowsTunnelService::timeout);
}

View File

@@ -8,6 +8,7 @@
#include <d3d11.h>
#include <dxgi.h>
#include <iphlpapi.h>
#include <shlobj_core.h>
#include <QDir>
#include <QHostAddress>
@@ -19,9 +20,9 @@
#include "logger.h"
#include "platforms/windows/windowsutils.h"
#define TUNNEL_SERVICE_NAME L"WireGuardTunnel$amnvpn"
constexpr const char* VPN_NAME = "AmneziaVPN";
constexpr const char* WIREGUARD_DIR = "WireGuard";
constexpr const char* DATA_DIR = "Data";
namespace {
Logger logger("WindowsCommons");
@@ -67,27 +68,67 @@ QString WindowsCommons::tunnelConfigFile() {
return QString();
}
// static
QString WindowsCommons::tunnelLogFile() {
QStringList paths =
QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation);
static QString tunnelLogFilePath = getTunnelLogFilePath();
return tunnelLogFilePath;
}
for (const QString& path : paths) {
QDir dir(path);
if (!dir.exists()) {
continue;
// static
QString WindowsCommons::getProgramFilesPath() {
wchar_t* path = nullptr;
if (SUCCEEDED(
SHGetKnownFolderPath(FOLDERID_ProgramFiles, 0, nullptr, &path))) {
auto guard = qScopeGuard([&] { CoTaskMemFree(path); });
return QString::fromWCharArray(path);
}
return QString();
}
// static
QString WindowsCommons::getTunnelLogFilePath() {
// Return WireGuard's log file path, "\Program Files\WireGuard\Data\log.bin",
// if the directory path exists
auto programFilesPath = getProgramFilesPath();
if (!programFilesPath.isEmpty()) {
QDir programFilesDir(programFilesPath);
if (programFilesDir.exists()) {
QDir wireGuardDir(programFilesDir.filePath(WIREGUARD_DIR));
if (wireGuardDir.exists()) {
QDir wireGuardDataDir(wireGuardDir.filePath(DATA_DIR));
if (wireGuardDataDir.exists()) {
return wireGuardDataDir.filePath("log.bin");
}
}
}
QDir vpnDir(dir.filePath(VPN_NAME));
if (!vpnDir.exists()) {
continue;
}
return vpnDir.filePath("log.bin");
}
logger.debug() << "Failed to find WireGuard Tunnel log file";
return QString();
}
// static
int WindowsCommons::AdapterIndexTo(const QHostAddress& dst) {
logger.debug() << "Getting Current Internet Adapter that routes to"
<< logger.sensitive(dst.toString());
quint32_be ipBigEndian;
quint32 ip = dst.toIPv4Address();
qToBigEndian(ip, &ipBigEndian);
_MIB_IPFORWARDROW routeInfo;
auto result = GetBestRoute(ipBigEndian, 0, &routeInfo);
if (result != NO_ERROR) {
return -1;
}
auto adapter =
QNetworkInterface::interfaceFromIndex(routeInfo.dwForwardIfIndex);
logger.debug() << "Internet Adapter:" << adapter.name();
return routeInfo.dwForwardIfIndex;
}
// static
int WindowsCommons::VPNAdapterIndex() {
// For someReason QNetworkInterface::fromName(MozillaVPN) does not work >:(
@@ -102,7 +143,7 @@ int WindowsCommons::VPNAdapterIndex() {
// Static
QString WindowsCommons::getCurrentPath() {
QByteArray buffer(2048, 0xFF);
QByteArray buffer(2048, 0xFFu);
auto ok = GetModuleFileNameA(NULL, buffer.data(), buffer.size());
if (ok == ERROR_INSUFFICIENT_BUFFER) {

View File

@@ -19,9 +19,14 @@ class WindowsCommons final {
// Returns the Interface Index of the VPN Adapter
static int VPNAdapterIndex();
// Returns the Interface Index that could Route to dst
static int AdapterIndexTo(const QHostAddress& dst);
// Returns the Path of the Current process
static QString getCurrentPath();
private:
static QString getTunnelLogFilePath();
static QString getProgramFilesPath();
};
#endif // WINDOWSCOMMONS_H

View File

@@ -4,6 +4,7 @@
#include "windowsnetworkwatcher.h"
#include <QNetworkInformation>
#include <QScopeGuard>
#include "leakdetector.h"
@@ -136,9 +137,4 @@ void WindowsNetworkWatcher::processWlan(PWLAN_NOTIFICATION_DATA data) {
logger.debug() << "Unsecure network:" << logger.sensitive(ssid)
<< "id:" << logger.sensitive(bssid);
emit unsecuredNetwork(ssid, bssid);
}
NetworkWatcherImpl::TransportType WindowsNetworkWatcher::getTransportType() {
// TODO: Implement this once we update to Qt6.3 (VPN-3511)
return TransportType_Other;
}
}

View File

@@ -17,8 +17,6 @@ class WindowsNetworkWatcher final : public NetworkWatcherImpl {
void initialize() override;
NetworkWatcherImpl::TransportType getTransportType() override;
private:
static void wlanCallback(PWLAN_NOTIFICATION_DATA data, PVOID context);

View File

@@ -7,6 +7,8 @@
#include <WS2tcpip.h>
#include <Windows.h>
#include <iphlpapi.h>
#include <winternl.h>
// Note: This important must come after the previous three.
// clang-format off
#include <IcmpAPI.h>
@@ -16,17 +18,58 @@
#include "leakdetector.h"
#include "logger.h"
#include "windowscommons.h"
#include "platforms/windows/windowsutils.h"
#include "windowscommons.h"
#pragma comment(lib, "Ws2_32")
/*
* On 64 Bit systems we need to use another struct.
*/
#ifdef _WIN64
using MZ_ICMP_ECHO_REPLY = ICMP_ECHO_REPLY32;
#else
using MZ_ICMP_ECHO_REPLY = ICMP_ECHO_REPLY;
#endif
constexpr WORD WindowsPingPayloadSize = sizeof(quint16);
constexpr size_t ICMP_ERR_SIZE = 8;
/*
* IcmpSendEcho2 expects us to provide a Buffer that is
* at least this size
*/
constexpr size_t MinimumReplyBufferSize =
sizeof(ICMP_ECHO_REPLY) + WindowsPingPayloadSize + ICMP_ERR_SIZE +
sizeof(IO_STATUS_BLOCK);
/**
* ICMP_ECHO_REPLY32 is smaller than ICMP_ECHO_REPLY, so if we use that due to
* binary compat Windows will add some padding.
*/
constexpr auto reply_padding =
sizeof(ICMP_ECHO_REPLY) - sizeof(MZ_ICMP_ECHO_REPLY);
// Disable Packing, so the compiler does not add padding in this struct between
// different sized types.
#pragma pack(push, 1)
struct ICMP_ECHO_REPLY_BUFFER {
MZ_ICMP_ECHO_REPLY reply;
std::array<uint8_t, reply_padding> padding;
quint16 payload;
std::array<char8_t, ICMP_ERR_SIZE> icmp_error;
IO_STATUS_BLOCK status;
};
#pragma pack(pop)
// If the Size is not the MinimumReplyBufferSize, the compiler added
// padding, so the fields will not be properly aligned with
// what IcmpSendEcho2 will write.
static_assert(sizeof(ICMP_ECHO_REPLY_BUFFER) == MinimumReplyBufferSize,
"Fulfills the size requirements");
struct WindowsPingSenderPrivate {
HANDLE m_handle;
HANDLE m_event;
unsigned char m_buffer[sizeof(ICMP_ECHO_REPLY) + WindowsPingPayloadSize + 8];
ICMP_ECHO_REPLY_BUFFER m_replyBuffer;
};
namespace {
@@ -58,7 +101,7 @@ WindowsPingSender::WindowsPingSender(const QHostAddress& source,
QObject::connect(m_notifier, &QWinEventNotifier::activated, this,
&WindowsPingSender::pingEventReady);
memset(m_private->m_buffer, 0, sizeof(m_private->m_buffer));
m_private->m_replyBuffer = {};
}
WindowsPingSender::~WindowsPingSender() {
@@ -86,16 +129,33 @@ void WindowsPingSender::sendPing(const QHostAddress& dest, quint16 sequence) {
quint32 v4dst = dest.toIPv4Address();
if (m_source.isNull()) {
IcmpSendEcho2(m_private->m_handle, m_private->m_event, nullptr, nullptr,
qToBigEndian<quint32>(v4dst), &sequence, sizeof(sequence),
nullptr, m_private->m_buffer, sizeof(m_private->m_buffer),
10000);
IcmpSendEcho2(m_private->m_handle, // IcmpHandle,
m_private->m_event, // Event
nullptr, // ApcRoutine
nullptr, // ApcContext
qToBigEndian<quint32>(v4dst), // DestinationAddress
&sequence, // RequestData
sizeof(sequence), // RequestSize
nullptr, // RequestOptions
&m_private->m_replyBuffer, // [OUT] ReplyBuffer
sizeof(m_private->m_replyBuffer), // ReplySize
10000 // Timeout
);
} else {
quint32 v4src = m_source.toIPv4Address();
IcmpSendEcho2Ex(m_private->m_handle, m_private->m_event, nullptr, nullptr,
qToBigEndian<quint32>(v4src), qToBigEndian<quint32>(v4dst),
&sequence, sizeof(sequence), nullptr, m_private->m_buffer,
sizeof(m_private->m_buffer), 10000);
IcmpSendEcho2Ex(m_private->m_handle, // IcmpHandle
m_private->m_event, // Event
nullptr, // ApcRoutine
nullptr, // ApcContext
qToBigEndian<quint32>(v4src), // SourceAddress
qToBigEndian<quint32>(v4dst), // DestinationAddress
&sequence, // RequestData
sizeof(sequence), // RequestSize
nullptr, // RequestOptions
&m_private->m_replyBuffer, // [OUT] ReplyBuffer
sizeof(m_private->m_replyBuffer), // ReplySize
10000 // Timeout
);
}
DWORD status = GetLastError();
@@ -108,8 +168,11 @@ void WindowsPingSender::sendPing(const QHostAddress& dest, quint16 sequence) {
}
void WindowsPingSender::pingEventReady() {
DWORD replyCount =
IcmpParseReplies(m_private->m_buffer, sizeof(m_private->m_buffer));
// Cleanup all data once we're done with m_replyBuffer.
const auto guard = qScopeGuard([this]() { m_private->m_replyBuffer = {}; });
DWORD replyCount = IcmpParseReplies(&m_private->m_replyBuffer,
sizeof(m_private->m_replyBuffer));
if (replyCount == 0) {
DWORD error = GetLastError();
if (error == IP_REQ_TIMED_OUT) {
@@ -120,14 +183,25 @@ void WindowsPingSender::pingEventReady() {
<< " Message: " << errmsg;
return;
}
const ICMP_ECHO_REPLY* replies = (const ICMP_ECHO_REPLY*)m_private->m_buffer;
for (DWORD i = 0; i < replyCount; i++) {
if (replies[i].DataSize < sizeof(quint16)) {
continue;
}
quint16 sequence;
memcpy(&sequence, replies[i].Data, sizeof(quint16));
emit recvPing(sequence);
// We only allocated for one reply, so more should be impossible.
if (replyCount != 1) {
logger.error() << "Invalid amount of responses recieved";
return;
}
if (m_private->m_replyBuffer.reply.Data == nullptr) {
logger.error() << "Did get a ping response without payload";
return;
}
// Assert that the (void*) pointer of Data is pointing
// to our ReplyBuffer payload.
if (m_private->m_replyBuffer.reply.Data == nullptr) {
logger.error() << "Did get a ping response without payload";
return;
}
// Assert that the (void*) pointer of Data is pointing
// to our ReplyBuffer payload.
assert(m_private->m_replyBuffer.reply.Data ==
static_cast<PVOID>(&m_private->m_replyBuffer.payload));
emit recvPing(m_private->m_replyBuffer.payload);
}

View File

@@ -1,5 +1,4 @@
#include "secure_qsettings.h"
#include "platforms/ios/MobileUtils.h"
#include "QAead.h"
#include "QBlockCipher.h"

View File

@@ -2944,8 +2944,8 @@ For more detailed information, you can
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="115"/>
<source>IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS.</source>
<translation>بروتوكول IKEv2 - بروتوكول مستقر حديث, اسرع بقليل من الباقي, يسترجع الاتصال بعد خسارة الاشارة. لدية يتمتع بدعم أصلي على أحدث إصدارات Android وiOS.</translation>
<source>IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss.</source>
<translation>بروتوكول IKEv2 - بروتوكول مستقر حديث, اسرع بقليل من الباقي, يسترجع الاتصال بعد خسارة الاشارة.</translation>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="118"/>

View File

@@ -2850,8 +2850,8 @@ While it offers a blend of security, stability, and speed, it&apos;s essential t
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="115"/>
<source>IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS.</source>
<translation>پروتکل IKEv2 پروتکلی پایدار و مدرن که مقداری سریعتر از سایر پروتکلهاست. بعد از قطع سیگنال دوباره اتصال را بازیابی میکند. به صورت پیشفرض بر روی آخرین نسخه دستگاههای اندروید و iOS پیشتیبانی میشود.</translation>
<source>IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss.</source>
<translation>پروتکل IKEv2 پروتکلی پایدار و مدرن که مقداری سریعتر از سایر پروتکلهاست. بعد از قطع سیگنال دوباره اتصال را بازیابی میکند.</translation>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="118"/>

View File

@@ -2998,8 +2998,8 @@ For more detailed information, you can
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="124"/>
<source>IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS.</source>
<translation>IKEv2 - ि ि , , ि ि ि Android iOS .</translation>
<source>IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss.</source>
<translation>IKEv2 - ि ि , , ि ि ि </translation>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="127"/>

View File

@@ -2851,8 +2851,8 @@ IKEv2 သည် လုံခြုံရေး၊ တည်ငြိမ်မှ
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="115"/>
<source>IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS.</source>
<translation>IKEv2 - က က signal ကက ကက . Android iOS က က.</translation>
<source>IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss.</source>
<translation>IKEv2 - က က signal ကက ကက .</translation>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="118"/>

View File

@@ -1088,7 +1088,17 @@ Already installed containers were found on the server. All installed containers
<translation>Язык</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApplication.qml" line="147"/>
<location filename="../ui/qml/Pages2/PageSettingsApplication.qml" line="218"/>
<source>Enable notifications</source>
<translation>Включить уведомления</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApplication.qml" line="219"/>
<source>Enable notifications to show the VPN state in the status bar</source>
<translation>Включить уведомления для отображения статуса VPN в строке состояния</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApplication.qml" line="238"/>
<source>Logging</source>
<translation>Логирование</translation>
</message>
@@ -2987,8 +2997,8 @@ While it offers a blend of security, stability, and speed, it&apos;s essential t
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="121"/>
<source>IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS.</source>
<translation>IKEv2 Современный стабильный протокол, немного быстрее других восстанавливает соединение после потери сигнала. Имеет нативную поддержку последних версиий Android и iOS.</translation>
<source>IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss.</source>
<translation>IKEv2 Современный стабильный протокол, немного быстрее других восстанавливает соединение после потери сигнала.</translation>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="124"/>

View File

@@ -3191,8 +3191,8 @@ While it offers a blend of security, stability, and speed, it&apos;s essential t
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="124"/>
<source>IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS.</source>
<translation>IKEv2 сучасний стабільний протокол, трішки швидше за інших відновлює підключення. Підтримується в останніх версіях Android и iOS самими операційними системами.</translation>
<source>IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss.</source>
<translation>IKEv2 сучасний стабільний протокол, трішки швидше за інших відновлює підключення.</translation>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="127"/>

View File

@@ -2947,8 +2947,8 @@ For more detailed information, you can
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="124"/>
<source>IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS.</source>
<translation>IKEv2 - جدید مستحکم پروٹوکول، دوسروں کے مقابلے میں تھوڑا تیز، سگنل ضائع ہونے کے بعد کنکشن بحال کرتا ہے۔ اسے اینڈرائیڈ اور آئی او ایس کے تازہ ترین ورژنز پر مقامی حمایت حاصل ہے۔</translation>
<source>IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss.</source>
<translation>IKEv2 - جدید مستحکم پروٹوکول، دوسروں کے مقابلے میں تھوڑا تیز، سگنل ضائع ہونے کے بعد کنکشن بحال کرتا ہے۔</translation>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="127"/>

View File

@@ -3057,8 +3057,8 @@ WireGuard非常容易被阻挡因为其独特的数据包签名。与一些
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="115"/>
<source>IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss. It has native support on the latest versions of Android and iOS.</source>
<translation>IKEv2 - Android iOS最新版原生支持</translation>
<source>IKEv2 - Modern stable protocol, a bit faster than others, restores connection after signal loss.</source>
<translation>IKEv2 - </translation>
</message>
<message>
<location filename="../containers/containers_defs.cpp" line="118"/>

View File

@@ -7,9 +7,8 @@
#endif
#include <QtConcurrent>
#include "core/controllers/apiController.h"
#include "core/controllers/vpnConfigurationController.h"
#include "core/errorstrings.h"
#include "version.h"
ConnectionController::ConnectionController(const QSharedPointer<ServersModel> &serversModel,
const QSharedPointer<ContainersModel> &containersModel,
@@ -17,6 +16,7 @@ ConnectionController::ConnectionController(const QSharedPointer<ServersModel> &s
const QSharedPointer<VpnConnection> &vpnConnection, const std::shared_ptr<Settings> &settings,
QObject *parent)
: QObject(parent),
m_apiController(this),
m_serversModel(serversModel),
m_containersModel(containersModel),
m_clientManagementModel(clientManagementModel),
@@ -27,70 +27,34 @@ ConnectionController::ConnectionController(const QSharedPointer<ServersModel> &s
connect(this, &ConnectionController::connectToVpn, m_vpnConnection.get(), &VpnConnection::connectToVpn, Qt::QueuedConnection);
connect(this, &ConnectionController::disconnectFromVpn, m_vpnConnection.get(), &VpnConnection::disconnectFromVpn, Qt::QueuedConnection);
connect(&m_apiController, &ApiController::configUpdated, this,
static_cast<void (ConnectionController::*)(const bool, const QJsonObject &, const int)>(&ConnectionController::openConnection));
connect(&m_apiController, qOverload<ErrorCode>(&ApiController::errorOccurred), this, qOverload<ErrorCode>(&ConnectionController::connectionErrorOccurred));
m_state = Vpn::ConnectionState::Disconnected;
}
void ConnectionController::openConnection()
{
int serverIndex = m_serversModel->getDefaultServerIndex();
auto serverConfig = m_serversModel->getServerConfig(serverIndex);
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
if (!Utils::processIsRunning(Utils::executable(SERVICE_NAME, false), true))
{
emit connectionErrorOccurred(ErrorCode::AmneziaServiceNotRunning);
return;
}
#endif
ErrorCode errorCode = ErrorCode::NoError;
int serverIndex = m_serversModel->getDefaultServerIndex();
QJsonObject serverConfig = m_serversModel->getServerConfig(serverIndex);
emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Preparing);
if (serverConfig.value(config_key::configVersion).toInt()
&& !m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) {
ApiController apiController;
errorCode = apiController.updateServerConfigFromApi(m_settings->getInstallationUuid(true), serverConfig);
if (errorCode != ErrorCode::NoError) {
emit connectionErrorOccurred(errorString(errorCode));
return;
}
m_serversModel->editServer(serverConfig, serverIndex);
m_apiController.updateServerConfigFromApi(m_settings->getInstallationUuid(true), serverIndex, serverConfig);
} else {
openConnection(false, serverConfig, serverIndex);
}
if (!m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) {
emit noInstalledContainers();
emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected);
return;
}
DockerContainer container = qvariant_cast<DockerContainer>(m_serversModel->data(serverIndex, ServersModel::Roles::DefaultContainerRole));
if (!m_containersModel->isSupportedByCurrentPlatform(container)) {
emit connectionErrorOccurred(tr("The selected protocol is not supported on the current platform"));
return;
}
if (container == DockerContainer::None) {
emit connectionErrorOccurred(tr("VPN Protocols is not installed.\n Please install VPN container at first"));
return;
}
qApp->processEvents();
QSharedPointer<ServerController> serverController(new ServerController(m_settings));
VpnConfigurationsController vpnConfigurationController(m_settings, serverController);
QJsonObject containerConfig = m_containersModel->getContainerConfig(container);
ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex);
errorCode = updateProtocolConfig(container, credentials, containerConfig, serverController);
if (errorCode != ErrorCode::NoError) {
emit connectionErrorOccurred(errorString(errorCode));
return;
}
auto dns = m_serversModel->getDnsPair(serverIndex);
serverConfig = m_serversModel->getServerConfig(serverIndex);
auto vpnConfiguration = vpnConfigurationController.createVpnConfiguration(dns, serverConfig, containerConfig, container, errorCode);
if (errorCode != ErrorCode::NoError) {
emit connectionErrorOccurred(tr("unable to create configuration"));
return;
}
emit connectToVpn(serverIndex, credentials, container, vpnConfiguration);
}
void ConnectionController::closeConnection()
@@ -98,9 +62,9 @@ void ConnectionController::closeConnection()
emit disconnectFromVpn();
}
QString ConnectionController::getLastConnectionError()
ErrorCode ConnectionController::getLastConnectionError()
{
return errorString(m_vpnConnection->lastError());
return m_vpnConnection->lastError();
}
void ConnectionController::onConnectionStateChanged(Vpn::ConnectionState state)
@@ -221,6 +185,53 @@ bool ConnectionController::isProtocolConfigExists(const QJsonObject &containerCo
return true;
}
void ConnectionController::openConnection(const bool updateConfig, const QJsonObject &config, const int serverIndex)
{
// Update config for this server as it was received from API
if (updateConfig) {
m_serversModel->editServer(config, serverIndex);
}
if (!m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) {
emit noInstalledContainers();
emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected);
return;
}
DockerContainer container = qvariant_cast<DockerContainer>(m_serversModel->data(serverIndex, ServersModel::Roles::DefaultContainerRole));
if (!m_containersModel->isSupportedByCurrentPlatform(container)) {
emit connectionErrorOccurred(tr("The selected protocol is not supported on the current platform"));
return;
}
if (container == DockerContainer::None) {
emit connectionErrorOccurred(tr("VPN Protocols is not installed.\n Please install VPN container at first"));
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);
ErrorCode errorCode = updateProtocolConfig(container, credentials, containerConfig, serverController);
if (errorCode != ErrorCode::NoError) {
emit connectionErrorOccurred(errorCode);
return;
}
auto dns = m_serversModel->getDnsPair(serverIndex);
auto vpnConfiguration = vpnConfigurationController.createVpnConfiguration(dns, config, containerConfig, container, errorCode);
if (errorCode != ErrorCode::NoError) {
emit connectionErrorOccurred(tr("unable to create configuration"));
return;
}
emit connectToVpn(serverIndex, credentials, container, vpnConfiguration);
}
ErrorCode ConnectionController::updateProtocolConfig(const DockerContainer container, const ServerCredentials &credentials,
QJsonObject &containerConfig, QSharedPointer<ServerController> serverController)
{

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