diff --git a/client/android/AndroidManifest.xml b/client/android/AndroidManifest.xml
index 62b91cead..e9ae60cec 100644
--- a/client/android/AndroidManifest.xml
+++ b/client/android/AndroidManifest.xml
@@ -68,6 +68,14 @@
android:taskAffinity=""
android:exported="false" />
+
+
diff --git a/client/android/protocolApi/src/main/kotlin/ProtocolState.kt b/client/android/protocolApi/src/main/kotlin/ProtocolState.kt
index d79949fa3..e66ed2ae1 100644
--- a/client/android/protocolApi/src/main/kotlin/ProtocolState.kt
+++ b/client/android/protocolApi/src/main/kotlin/ProtocolState.kt
@@ -4,6 +4,5 @@ enum class ProtocolState {
CONNECTED,
CONNECTING,
DISCONNECTED,
- DISCONNECTING,
- UNKNOWN
+ DISCONNECTING
}
diff --git a/client/android/res/values/styles.xml b/client/android/res/values/styles.xml
index 6358e68d2..9f4201f89 100644
--- a/client/android/res/values/styles.xml
+++ b/client/android/res/values/styles.xml
@@ -4,4 +4,14 @@
- false
- true
+
\ No newline at end of file
diff --git a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt
index 148c7eeb1..40b504530 100644
--- a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt
+++ b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt
@@ -6,13 +6,11 @@ import android.content.ServiceConnection
import android.net.Uri
import android.net.VpnService
import android.os.Bundle
-import android.os.DeadObjectException
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.os.Message
import android.os.Messenger
-import android.os.RemoteException
import android.widget.Toast
import androidx.annotation.MainThread
import androidx.core.content.ContextCompat
@@ -76,6 +74,9 @@ class AmneziaActivity : QtActivity() {
}
ServiceEvent.ERROR -> {
+ msg.data?.getString(ERROR_MSG)?.let { error ->
+ Log.e(TAG, "From VpnService: $error")
+ }
// todo: add error reporting to Qt
QtAndroidController.onServiceError()
}
diff --git a/client/android/src/org/amnezia/vpn/AmneziaApplication.kt b/client/android/src/org/amnezia/vpn/AmneziaApplication.kt
index df8b02ae5..e9d8fdfb7 100644
--- a/client/android/src/org/amnezia/vpn/AmneziaApplication.kt
+++ b/client/android/src/org/amnezia/vpn/AmneziaApplication.kt
@@ -3,14 +3,32 @@ 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.qtproject.qt.android.bindings.QtApplication
+const val NOTIFICATION_CHANNEL_ID: String = "org.amnezia.vpn.notification"
+
class AmneziaApplication : QtApplication(), CameraXConfig.Provider {
+ override fun onCreate() {
+ super.onCreate()
+ createNotificationChannel()
+ }
+
override fun getCameraXConfig(): CameraXConfig = CameraXConfig.Builder
.fromConfig(Camera2Config.defaultConfig())
.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()
+ )
+ }
}
diff --git a/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt b/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt
index 8c7815474..58f107198 100644
--- a/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt
+++ b/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt
@@ -1,983 +1,350 @@
-/* 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.content.Context
+import android.app.Notification
+import android.app.PendingIntent
import android.content.Intent
-import android.content.pm.PackageManager
-import android.net.LocalSocket
-import android.net.LocalSocketAddress
-import android.net.Network
-import android.net.ProxyInfo
-import android.os.*
-import android.system.ErrnoException
-import android.system.Os
-import android.system.OsConstants
-import android.text.TextUtils
-import androidx.core.content.FileProvider
-import com.wireguard.android.util.SharedLibraryLoader
-import com.wireguard.config.*
-import com.wireguard.crypto.Key
-import com.wireguard.android.backend.GoBackend
+import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
+import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
+import android.net.VpnService
+import android.os.Build
+import android.os.Handler
+import android.os.IBinder
+import android.os.Looper
+import android.os.Message
+import android.os.Messenger
+import androidx.annotation.MainThread
+import androidx.core.app.NotificationCompat
+import androidx.core.app.ServiceCompat
+import kotlin.LazyThreadSafetyMode.NONE
+import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.delay
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.cancelAndJoin
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
-// import org.amnezia.vpn.shadowsocks.core.Core
-// import org.amnezia.vpn.shadowsocks.core.R
-// import org.amnezia.vpn.shadowsocks.core.VpnRequestActivity
-// import org.amnezia.vpn.shadowsocks.core.acl.Acl
-// import org.amnezia.vpn.shadowsocks.core.bg.*
-// import org.amnezia.vpn.shadowsocks.core.database.Profile
-// import org.amnezia.vpn.shadowsocks.core.database.ProfileManager
-// import org.amnezia.vpn.shadowsocks.core.net.ConcurrentLocalSocketListener
-// import org.amnezia.vpn.shadowsocks.core.net.DefaultNetworkListener
-// import org.amnezia.vpn.shadowsocks.core.net.Subnet
-// import org.amnezia.vpn.shadowsocks.core.preference.DataStore
-// import org.amnezia.vpn.shadowsocks.core.utils.Key.modeVpn
-// import org.amnezia.vpn.shadowsocks.core.utils.printLog
+import org.amnezia.vpn.protocol.BadConfigException
+import org.amnezia.vpn.protocol.LoadLibraryException
+import org.amnezia.vpn.protocol.Protocol
+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.Statistics
+import org.amnezia.vpn.protocol.Status
+import org.amnezia.vpn.protocol.VpnStartException
+import org.amnezia.vpn.protocol.putStatistics
+import org.amnezia.vpn.protocol.putStatus
+import org.amnezia.vpn.protocol.wireguard.Wireguard
+import org.json.JSONException
import org.json.JSONObject
-import java.io.Closeable
-import java.io.File
-import java.io.FileDescriptor
-import java.io.IOException
-import java.lang.Exception
-import android.net.VpnService as BaseVpnService
+
+private const val TAG = "AmneziaVpnService"
const val VPN_CONFIG = "VPN_CONFIG"
+const val ERROR_MSG = "ERROR_MSG"
+const val AFTER_PERMISSION_CHECK = "AFTER_PERMISSION_CHECK"
+private const val PREFS_CONFIG_KEY = "LAST_CONF"
+private const val NOTIFICATION_ID = 1337
-class AmneziaVpnService : BaseVpnService()/* , LocalDnsService.Interface */ {
+class AmneziaVpnService : VpnService() {
- // override val data = BaseService.Data(this)
-
- /* override */ val tag: String get() = "AmneziaVpnService"
-// override fun createNotification(profileName: String): ServiceNotification =
-// ServiceNotification(this, profileName, "service-vpn")
+ private lateinit var mainScope: CoroutineScope
+ private var isServiceBound = false
+ private var protocol: Protocol? = null
+ private var protocolState = MutableStateFlow(DISCONNECTED)
- private var conn: ParcelFileDescriptor? = null
- // private var worker: ProtectWorker? = null
- private var active = false
- private var metered = false
- private var mNetworkState = NetworkState(this)
- private var underlyingNetwork: Network? = null
- set(value) {
- field = value
- if (active && Build.VERSION.SDK_INT >= 22) setUnderlyingNetworks(underlyingNetworks)
+ private val isConnected
+ get() = protocolState.value == CONNECTED
+
+ private val isDisconnected
+ get() = protocolState.value == DISCONNECTED
+
+ private var connectionJob: Job? = null
+ private var disconnectionJob: Job? = null
+ private lateinit var clientMessenger: IpcMessenger
+
+ private val connectionExceptionHandler = CoroutineExceptionHandler { _, e ->
+ protocolState.value = DISCONNECTED
+ protocol = null
+ when (e) {
+ is IllegalArgumentException,
+ is VpnStartException -> onError(e.message ?: e.toString())
+
+ is JSONException,
+ is BadConfigException -> onError("VPN config format error: ${e.message}")
+
+ is LoadLibraryException -> onError("${e.message}. Caused: ${e.cause?.message}")
+
+ else -> throw e
}
- private val underlyingNetworks
- get() =
- // clearing underlyingNetworks makes Android 9+ consider the network to be metered
- if (Build.VERSION.SDK_INT >= 28 && metered) null else underlyingNetwork?.let {
- arrayOf(
- it
+ }
+
+ private val actionMessageHandler: Handler by lazy(NONE) {
+ object : Handler(Looper.getMainLooper()) {
+ override fun handleMessage(msg: Message) {
+ val action = msg.extractIpcMessage()
+ Log.d(TAG, "Handle action: $action")
+ when (action) {
+ Action.REGISTER_CLIENT -> {
+ clientMessenger.set(msg.replyTo)
+ }
+
+ Action.CONNECT -> {
+ val vpnConfig = msg.data.getString(VPN_CONFIG)
+ saveConfigToPrefs(vpnConfig)
+ connect(vpnConfig)
+ }
+
+ Action.DISCONNECT -> {
+ disconnect()
+ }
+
+ Action.REQUEST_STATUS -> {
+ clientMessenger.send {
+ ServiceEvent.STATUS.packToMessage {
+ putStatus(Status.build {
+ setConnected(this@AmneziaVpnService.isConnected)
+ })
+ }
+ }
+ }
+
+ Action.REQUEST_STATISTICS -> {
+ clientMessenger.send {
+ ServiceEvent.STATISTICS_UPDATE.packToMessage {
+ putStatistics(protocol?.statistics ?: Statistics.EMPTY_STATISTICS)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private val vpnServiceMessenger: Messenger by lazy(NONE) {
+ Messenger(actionMessageHandler)
+ }
+
+ /**
+ * Notification setup
+ */
+ private val foregroundServiceTypeCompat
+ get() = when {
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> FOREGROUND_SERVICE_TYPE_SPECIAL_USE
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> FOREGROUND_SERVICE_TYPE_MANIFEST
+ 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
)
- }
-
- val handler = Handler(Looper.getMainLooper())
- var runnable: Runnable = object : Runnable {
- override fun run() {
- if (mProtocol.equals("shadowsocks", true)) {
- /* Log.e(tag, "run: -----------------: ${data.state}")
- when (data.state) {
- BaseService.State.Connected -> {
- currentTunnelHandle = 1
- isUp = true
- }
- BaseService.State.Stopped -> {
- currentTunnelHandle = -1
- isUp = false
- }
- else -> {
-
- }
- } */
- }
- handler.postDelayed(this, 1000L) //wait 4 sec and run again
- }
- }
-
- fun stopTest() {
- handler.removeCallbacks(runnable)
- }
-
- fun startTest() {
- handler.postDelayed(runnable, 0) //wait 0 ms and run
- }
-
- companion object {
- private const val VPN_MTU = 1500
- private const val PRIVATE_VLAN4_CLIENT = "172.19.0.1"
- private const val PRIVATE_VLAN4_ROUTER = "172.19.0.2"
- private const val PRIVATE_VLAN6_CLIENT = "fdfe:dcba:9876::1"
- private const val PRIVATE_VLAN6_ROUTER = "fdfe:dcba:9876::2"
-
- /**
- * https://android.googlesource.com/platform/prebuilts/runtime/+/94fec32/appcompat/hiddenapi-light-greylist.txt#9466
- */
- private val getInt = FileDescriptor::class.java.getDeclaredMethod("getInt$")
-
- @JvmStatic
- fun startService(c: Context) {
- c.applicationContext.startService(
- Intent(c.applicationContext, AmneziaVpnService::class.java).apply {
- putExtra("startOnly", true)
- })
- }
-
- }
-
- private var mBinder: VPNServiceBinder = VPNServiceBinder(this)
- private var mConfig: JSONObject? = null
- private var mProtocol: String? = null
- private var mConnectionTime: Long = 0
- private var mAlreadyInitialised = false
- private var mbuilder: Builder = Builder()
-
- private var mOpenVPNThreadv3: OpenVPNThreadv3? = null
- var currentTunnelHandle = -1
-
- private var intent: Intent? = null
- private var flags = 0
- private var startId = 0
-
- fun init() {
- if (mAlreadyInitialised) {
- return
- }
- // Log.init(this)
- SharedLibraryLoader.loadSharedLibrary(this, "wg-go")
- SharedLibraryLoader.loadSharedLibrary(this, "ovpn3")
- Log.i(tag, "Loaded libs")
- Log.e(tag, "Wireguard Version ${GoBackend.wgVersion()}")
- mOpenVPNThreadv3 = OpenVPNThreadv3(this)
- mAlreadyInitialised = true
+ )
+ .setOngoing(true)
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ .setCategory(NotificationCompat.CATEGORY_SERVICE)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
+ .build()
}
+ /**
+ * Service overloaded methods
+ */
override fun onCreate() {
- super.onCreate()
- NotificationUtil.show(this) // Go foreground
+ super.onCreate()
+ Log.v(TAG, "Create Amnezia VPN service")
+ mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate + connectionExceptionHandler)
+ clientMessenger = IpcMessenger(messengerName = "Client")
+ launchProtocolStateHandler()
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ val isAlwaysOnCompat =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) isAlwaysOn
+ else intent?.component?.packageName != packageName
+
+ if (isAlwaysOnCompat) {
+ Log.v(TAG, "Start service via Always-on")
+ connect(loadConfigFromPrefs())
+ } else if (intent?.getBooleanExtra(AFTER_PERMISSION_CHECK, false) == true) {
+ Log.v(TAG, "Start service after permission check")
+ connect(loadConfigFromPrefs())
+ } else {
+ Log.v(TAG, "Start service")
+ val vpnConfig = intent?.getStringExtra(VPN_CONFIG)
+ saveConfigToPrefs(vpnConfig)
+ connect(vpnConfig)
+ }
+ ServiceCompat.startForeground(this, NOTIFICATION_ID, notification, foregroundServiceTypeCompat)
+ return START_REDELIVER_INTENT
+ }
+
+ override fun onBind(intent: Intent?): IBinder? {
+ Log.d(TAG, "onBind by $intent")
+ if (intent?.action == "android.net.VpnService") return super.onBind(intent)
+ isServiceBound = true
+ return vpnServiceMessenger.binder
}
override fun onUnbind(intent: Intent?): Boolean {
- if (!isUp) {
- // If the Qt Client got closed while we were not connected
- // we do not need to stay as a foreground service.
- stopForeground(true)
+ Log.d(TAG, "onUnbind by $intent")
+ if (intent?.action != "android.net.VpnService") {
+ isServiceBound = false
+ clientMessenger.reset()
+ if (isDisconnected) stopSelf()
}
return super.onUnbind(intent)
}
- override fun onDestroy() {
- turnOff()
+ override fun onRevoke() {
+ Log.v(TAG, "onRevoke")
+ mainScope.launch {
+ disconnect()
+ }
+ }
+ override fun onDestroy() {
+ Log.v(TAG, "Destroy service")
+ if (!isDisconnected) {
+ protocol?.stopVpn()
+ protocolState.value = DISCONNECTED
+ }
+ mainScope.cancel()
super.onDestroy()
}
/**
- * EntryPoint for the Service, gets Called when AndroidController.cpp
- * calles bindService. Returns the [VPNServiceBinder] so QT can send Requests to it.
+ * Methods responsible for processing VPN connection
*/
- override fun onBind(intent: Intent): IBinder {
-
- when (mProtocol) {
- "shadowsocks" -> {
- /* when (intent.action) {
- SERVICE_INTERFACE -> super.onBind(intent)
- else -> super.onBind(intent)
- }
- startTest() */
- }
- else -> {
- init()
- }
- }
-
- return mBinder
- }
-
- /**
- * Might be the entryPoint if the Service gets Started via an
- * Service Intent: Might be from Always-On-Vpn from Settings
- * or from Booting the device and having "connect on boot" enabled.
- */
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- this.intent = intent
- this.flags = flags
- this.startId = startId
- init()
- intent?.let {
- if (!isUp && intent.getBooleanExtra("startOnly", false)) {
- Log.i(tag, "Start only!")
- return START_REDELIVER_INTENT
-// return super.onStartCommand(intent, flags, startId)
- }
- }
- // This start is from always-on
- if (this.mConfig == null) {
- // We don't have tunnel to turn on - Try to create one with last config the service got
- val prefs = Prefs.get(this)
- val lastConfString = prefs.getString("lastConf", "")
- if (lastConfString.isNullOrEmpty()) {
- // We have nothing to connect to -> Exit
- Log.e(tag, "VPN service was triggered without defining a Server or having a tunnel")
- return super.onStartCommand(intent, flags, startId)
- }
- this.mConfig = JSONObject(lastConfString)
- }
-
- mProtocol = mConfig!!.getString("protocol")
- Log.e(tag, "mProtocol: $mProtocol")
- if (mProtocol.equals("cloak", true) || (mProtocol.equals("openvpn", true))) {
- startOpenVpn()
- mNetworkState.bindNetworkListener()
- }
- /* if (mProtocol.equals("shadowsocks", true)) {
- if (DataStore.serviceMode == modeVpn) {
- if (prepare(this) != null) {
- startActivity(
- Intent(
- this,
- VpnRequestActivity::class.java
- ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- )
- } else {
- Log.e(tag, "Else part enter")
-// service?.startListeningForBandwidth(serviceCallback, 1000)
- Log.e(tag, "test")
- return super.onStartCommand(intent, flags, startId)
- }
- }
- stopRunner()
- } */
- return START_REDELIVER_INTENT
- }
-
- // Invoked when the application is revoked.
- // At this moment, the VPN interface is already deactivated by the system.
- override fun onRevoke() {
- Log.v(tag, "Aman: onRevoke....................")
- //this.turnOff()
- super.onRevoke()
- }
-
- var connectionTime: Long = 0
- get() {
- return mConnectionTime
- }
-
- var isUp: Boolean = false
- get() {
- return when (mProtocol) {
- "cloak",
- "openvpn" -> {
- field
- }
- else -> {
- currentTunnelHandle >= 0
- }
- }
- }
- set(value) {
- field = value
-
- if (value) {
- mBinder.dispatchEvent(VPNServiceBinder.EVENTS.connected, "")
- mConnectionTime = System.currentTimeMillis()
- return
- }
- mBinder.dispatchEvent(VPNServiceBinder.EVENTS.disconnected, "")
- mConnectionTime = 0
- }
-
- val status: JSONObject
- get() {
- val deviceIpv4: String = ""
-
- val status = when (mProtocol) {
- "cloak",
- "openvpn" -> {
- if (mOpenVPNThreadv3 == null) {
- Status(null, null, null, null)
- } else {
- val rx = mOpenVPNThreadv3?.getTotalRxBytes() ?: ""
- val tx = mOpenVPNThreadv3?.getTotalTxBytes() ?: ""
-
- Status(
- rx.toString(),
- tx.toString(),
- if (mConfig!!.has("server")) { mConfig?.getJSONObject("server")?.getString("ipv4Gateway") } else {""},
- if (mConfig!!.has("device")) { mConfig?.getJSONObject("device")?.getString("ipv4Address") } else {""}
- )
+ private fun launchProtocolStateHandler() {
+ mainScope.launch {
+ protocolState.collect { protocolState ->
+ Log.d(TAG, "Protocol state: $protocolState")
+ when (protocolState) {
+ CONNECTED -> {
+ clientMessenger.send(ServiceEvent.CONNECTED)
}
- }
- else -> {
- Status(
- getConfigValue("rx_bytes"),
- getConfigValue("tx_bytes"),
- if (mConfig!!.has("server")) { mConfig?.getJSONObject("server")?.getString("ipv4Gateway") } else {""},
- if (mConfig!!.has("server")) {mConfig?.getJSONObject("device")?.getString("ipv4Address") } else {""}
- )
- }
- }
- return JSONObject().apply {
- putOpt("rx_bytes", status.rxBytes)
- putOpt("tx_bytes", status.txBytes)
- putOpt("endpoint", status.endpoint)
- putOpt("deviceIpv4", status.device)
- }
- }
-
- data class Status(
- var rxBytes: String?,
- var txBytes: String?,
- var endpoint: String?,
- var device: String?
- )
-
- /*
- * Checks if the VPN Permission is given.
- * If the permission is given, returns true
- * Requests permission and returns false if not.
- */
- fun checkPermissions(): Boolean {
- // See https://developer.android.com/guide/topics/connectivity/vpn#connect_a_service
- // Call Prepare, if we get an Intent back, we dont have the VPN Permission
- // from the user. So we need to pass this to our main Activity and exit here.
- val intent = prepare(this)
- if (intent == null) {
- Log.e(tag, "VPN Permission Already Present")
- return true
- }
- Log.e(tag, "Requesting VPN Permission")
- return false
- }
-
- fun turnOn(json: JSONObject?): Int {
- Log.v(tag, "Aman: turnOn....................")
- if (!checkPermissions()) {
- Log.e(tag, "turn on was called without no permissions present!")
- isUp = false
- return 0
- }
- Log.i(tag, "Permission okay")
- mConfig = json!!
- Log.i(tag, "Config: $mConfig")
- mProtocol = mConfig!!.getString("protocol")
- Log.i(tag, "Protocol: $mProtocol")
-
- when (mProtocol) {
- "cloak",
- "openvpn" -> {
- startOpenVpn()
- // Store the config in case the service gets
- // asked boot vpn from the OS
- val prefs = Prefs.get(this)
- prefs.edit()
- .putString("lastConf", mConfig.toString())
- .apply()
-
- mNetworkState.bindNetworkListener()
- }
- "wireguard" -> {
- startWireGuard("wireguard")
- }
- "awg" -> {
- startWireGuard("awg")
- }
- /* "shadowsocks" -> {
- startShadowsocks()
- startTest()
- } */
- else -> {
- Log.e(tag, "No protocol")
- return 0
- }
- }
- NotificationUtil.show(this)
- return 1
- }
-
- fun establish(): ParcelFileDescriptor? {
- Log.v(tag, "Aman: establish....................")
- mbuilder.allowFamily(OsConstants.AF_INET)
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) mbuilder.setMetered(false)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) setUnderlyingNetworks(null)
-
- return mbuilder.establish()
- }
-
- fun setMtu(mtu: Int) {
- mbuilder.setMtu(mtu)
- }
-
- fun addAddress(ip: String, len: Int) {
- Log.v(tag, "mbuilder.addAddress($ip, $len)")
- mbuilder.addAddress(ip, len)
- }
-
- fun addRoute(ip: String, len: Int) {
- Log.v(tag, "mbuilder.addRoute($ip, $len)")
- mbuilder.addRoute(ip, len)
- }
-
- fun addDNS(ip: String) {
- Log.v(tag, "mbuilder.addDnsServer($ip)")
- mbuilder.addDnsServer(ip)
- if ("samsung".equals(Build.BRAND) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- mbuilder.addRoute(ip, 32)
- }
- }
-
- fun networkChange() {
- Log.i(tag, "mProtocol $mProtocol")
- if (isUp){
- mbuilder = Builder()
- mOpenVPNThreadv3?.reconnect(0)
- }
- }
-
- fun setSessionName(name: String) {
- Log.v(tag, "mbuilder.setSession($name)")
- mbuilder.setSession(name)
- }
-
- fun addHttpProxy(host: String, port: Int): Boolean {
- val proxyInfo = ProxyInfo.buildDirectProxy(host, port)
- Log.v(tag, "mbuilder.addHttpProxy($host, $port)")
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- mbuilder.setHttpProxy(proxyInfo)
- }
- return true
- }
-
- fun setDomain(domain: String) {
- Log.v(tag, "mbuilder.setDomain($domain)")
- mbuilder.addSearchDomain(domain)
- }
-
- fun turnOff() {
- Log.v(tag, "Aman: turnOff....................")
- when (mProtocol) {
- "wireguard",
- "awg" -> {
- GoBackend.wgTurnOff(currentTunnelHandle)
- }
- "cloak",
- "openvpn" -> {
- ovpnTurnOff()
- mNetworkState.unBindNetworkListener()
- }
- /* "shadowsocks" -> {
- stopRunner(false)
- stopTest()
- } */
- else -> {
- Log.e(tag, "No protocol")
- }
- }
-
- currentTunnelHandle = -1
- stopForeground(true)
- isUp = false
- stopSelf()
- }
-
-
- private fun ovpnTurnOff() {
- mOpenVPNThreadv3?.stop()
- mOpenVPNThreadv3 = null
- Log.e(tag, "mOpenVPNThreadv3 stop!")
- }
-
- /**
- * Configures an Android VPN Service Tunnel
- * with a given Wireguard Config
- */
- private fun setupBuilder(config: Config, builder: Builder) {
- // Setup Split tunnel
- for (excludedApplication in config.`interface`.excludedApplications)
- builder.addDisallowedApplication(excludedApplication)
-
- // Device IP
- for (addr in config.`interface`.addresses) builder.addAddress(addr.address, addr.mask)
- // DNS
- for (addr in config.`interface`.dnsServers) builder.addDnsServer(addr.hostAddress)
- // Add All routes the VPN may route tos
- for (peer in config.peers) {
- for (addr in peer.allowedIps) {
- builder.addRoute(addr.address, addr.mask)
- }
- }
- builder.allowFamily(OsConstants.AF_INET)
- builder.allowFamily(OsConstants.AF_INET6)
- builder.setMtu(config.`interface`.mtu.orElse(1280))
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) builder.setMetered(false)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) setUnderlyingNetworks(null)
-
- builder.setBlocking(true)
- }
-
- /**
- * Gets config value for {key} from the Current
- * running Wireguard tunnel
- */
- private fun getConfigValue(key: String): String? {
- if (!isUp) {
- return null
- }
- val config = GoBackend.wgGetConfig(currentTunnelHandle) ?: return null
- val lines = config.split("\n")
- for (line in lines) {
- val parts = line.split("=")
- val k = parts.first()
- val value = parts.last()
- if (key == k) {
- return value
- }
- }
- return null
- }
-
- private fun parseConfigData(data: String): Map> {
- val parseData = mutableMapOf>()
- var currentSection: Pair>? = null
- data.lines().forEach { line ->
- if (line.isNotEmpty()) {
- if (line.startsWith('[')) {
- currentSection?.let {
- parseData.put(it.first, it.second)
+ DISCONNECTED -> {
+ clientMessenger.send(ServiceEvent.DISCONNECTED)
+ if (!isServiceBound) stopSelf()
}
- currentSection =
- line.substring(1, line.indexOfLast { it == ']' }) to mutableMapOf()
- } else {
- val parameter = line.split("=", limit = 2)
- currentSection!!.second.put(parameter.first().trim(), parameter.last().trim())
+
+ CONNECTING, DISCONNECTING -> {}
}
}
}
- currentSection?.let {
- parseData.put(it.first, it.second)
- }
- return parseData
- }
-
-
- /**
- * Create a Wireguard [Config] from a [json] string -
- * The [json] will be created in AndroidVpnProtocol.cpp
- */
- private fun buildWireguardConfig(obj: JSONObject, type: String): Config {
- val confBuilder = Config.Builder()
- val wireguardConfigData = obj.getJSONObject(type)
- val splitTunnelType = obj.getInt("splitTunnelType")
- val splitTunnelSites = obj.getJSONArray("splitTunnelSites")
-
- val config = parseConfigData(wireguardConfigData.getString("config"))
- val peerBuilder = Peer.Builder()
- val peerConfig = config["Peer"]!!
- peerBuilder.setPublicKey(Key.fromBase64(peerConfig["PublicKey"]))
- peerConfig["PresharedKey"]?.let { peerBuilder.setPreSharedKey(Key.fromBase64(it)) }
-
- val allIpString = peerConfig["AllowedIPs"]
-
- var allowedIPList = peerConfig["AllowedIPs"]?.split(",") ?: emptyList()
-
- /* default value in template */
- if (allIpString == "0.0.0.0/0, ::/0") {
- allowedIPList = emptyList()
- }
-
- if (allowedIPList.isEmpty() && (splitTunnelType == 0)) {
- /* AllowedIP is empty and splitTunnel is turnoff */
- /* use VPN for whole Internet */
- val internetV4 = InetNetwork.parse("0.0.0.0/0") // aka The whole internet.
- peerBuilder.addAllowedIp(internetV4)
- val internetV6 = InetNetwork.parse("::/0") // aka The whole internet.
- peerBuilder.addAllowedIp(internetV6)
- } else {
- if (!allowedIPList.isEmpty()) {
- /* We have predefined AllowedIP in WG config */
- /* It's have higher priority than system SplitTunnel */
- allowedIPList.forEach {
- val network = InetNetwork.parse(it.trim())
- peerBuilder.addAllowedIp(network)
- }
- } else {
- if (splitTunnelType == 1) {
- /* Use system SplitTunnel */
- /* VPN connection used only for defined IPs */
- for (i in 0 until splitTunnelSites.length()) {
- val site = splitTunnelSites.getString(i)
- val internet = InetNetwork.parse(site)
- peerBuilder.addAllowedIp(internet)
- }
- }
- if (splitTunnelType == 2) {
- /* Use system SplitTunnel */
- /* VPN connection used for all Internet exclude defined IPs */
- val ipRangeSet = IPRangeSet.fromString("0.0.0.0/0")
- ipRangeSet.remove(IPRange("127.0.0.0/8"))
- for (i in 0 until splitTunnelSites.length()) {
- val site = splitTunnelSites.getString(i)
- ipRangeSet.remove(IPRange(site))
- }
- val allowedIps = ipRangeSet.subnets().joinToString(", ") + ", 2000::/3"
- peerBuilder.parseAllowedIPs(allowedIps)
- }
- }
- }
- val endpointConfig = peerConfig["Endpoint"]
- val endpoint = InetEndpoint.parse(endpointConfig)
- peerBuilder.setEndpoint(endpoint)
- peerConfig["PersistentKeepalive"]?.let { peerBuilder.setPersistentKeepalive(it.toInt()) }
- confBuilder.addPeer(peerBuilder.build())
-
- val ifaceBuilder = Interface.Builder()
- val ifaceConfig = config["Interface"]!!
- ifaceBuilder.parsePrivateKey(ifaceConfig["PrivateKey"])
- ifaceBuilder.addAddress(InetNetwork.parse(ifaceConfig["Address"]))
- ifaceConfig["DNS"]!!.split(",").forEach {
- ifaceBuilder.addDnsServer(InetNetwork.parse(it.trim()).address)
- }
-
- ifaceBuilder.parsePrivateKey(ifaceConfig["PrivateKey"])
- if (type == "awg_config_data") {
- ifaceBuilder.parseJc(ifaceConfig["Jc"])
- ifaceBuilder.parseJmin(ifaceConfig["Jmin"])
- ifaceBuilder.parseJmax(ifaceConfig["Jmax"])
- ifaceBuilder.parseS1(ifaceConfig["S1"])
- ifaceBuilder.parseS2(ifaceConfig["S2"])
- ifaceBuilder.parseH1(ifaceConfig["H1"])
- ifaceBuilder.parseH2(ifaceConfig["H2"])
- ifaceBuilder.parseH3(ifaceConfig["H3"])
- ifaceBuilder.parseH4(ifaceConfig["H4"])
- } else {
- ifaceBuilder.parseJc("0")
- ifaceBuilder.parseJmin("0")
- ifaceBuilder.parseJmax("0")
- ifaceBuilder.parseS1("0")
- ifaceBuilder.parseS2("0")
- ifaceBuilder.parseH1("0")
- ifaceBuilder.parseH2("0")
- ifaceBuilder.parseH3("0")
- ifaceBuilder.parseH4("0")
- }
- /*val jExcludedApplication = obj.getJSONArray("excludedApps")
- (0 until jExcludedApplication.length()).toList().forEach {
- val appName = jExcludedApplication.get(it).toString()
- ifaceBuilder.excludeApplication(appName)
- }*/
- confBuilder.setInterface(ifaceBuilder.build())
-
- return confBuilder.build()
}
- fun getVpnConfig(): JSONObject {
- return mConfig!!
- }
+ @MainThread
+ private fun connect(vpnConfig: String?) {
+ Log.v(TAG, "Start VPN connection")
- /* private fun startShadowsocks() {
- Log.e(tag, "startShadowsocks method enters")
- if (mConfig != null) {
- try {
- Log.e(tag, "Config: $mConfig")
+ if (isConnected || protocolState.value == CONNECTING) return
- ProfileManager.clear()
- val profile = Profile()
-// val iter: Iterator = mConfig!!.keys()
-// while (iter.hasNext()) {
-// val key = iter.next()
-// try {
-// val value: Any = mConfig!!.get(key)
-// Log.i(tag, "startShadowsocks: $key : $value")
-// } catch (e: JSONException) {
-// // Something went wrong!
-// }
-// }
+ protocolState.value = CONNECTING
- val shadowsocksConfig = mConfig?.getJSONObject("shadowsocks_config_data")
-
- if (shadowsocksConfig?.has("name") == true) {
- profile.name = shadowsocksConfig.getString("name")
- } else {
- profile.name = "amnezia"
- }
- if (shadowsocksConfig?.has("method") == true) {
- profile.method = shadowsocksConfig.getString("method").toString()
- }
- if (shadowsocksConfig?.has("server") == true) {
- profile.host = shadowsocksConfig.getString("server").toString()
- }
- if (shadowsocksConfig?.has("password") == true) {
- profile.password = shadowsocksConfig.getString("password").toString()
- }
- if (shadowsocksConfig?.has("server_port") == true) {
- profile.remotePort = shadowsocksConfig.getInt("server_port")
- }
-// if(mConfig?.has("local_port") == true) {
-// profile. = mConfig?.getInt("local_port")
-// }
-// profile.name = "amnezia"
-// profile.method = "chacha20-ietf-poly1305"
-// profile.host = "de01-ss.sshocean.net"
-// profile.password = "ZTZhN"
-// profile.remotePort = 8388
-
- profile.proxyApps = false
- profile.bypass = false
- profile.metered = false
- profile.dirty = false
- profile.ipv6 = true
-
- DataStore.profileId = ProfileManager.createProfile(profile).id
- val switchProfile = Core.switchProfile(DataStore.profileId)
- Log.i(tag, "startShadowsocks: SwitchProfile: $switchProfile")
- intent?.putExtra("startOnly", false)
- onStartCommand(
- intent,
- flags,
- startId
- )
-// startRunner()
-// VpnManager.getInstance().run()
-// VpnManager.getInstance()
-// .setOnStatusChangeListener(object : VpnManager.OnStatusChangeListener {
-// override fun onStatusChanged(state: BaseService.State) {
-// when (state) {
-// BaseService.State.Connected -> {
-// isUp = true
-// }
-// BaseService.State.Stopped -> {
-// isUp = false
-// }
-// else -> {}
-// }
-// }
-//
-// override fun onTrafficUpdated(profileId: Long, stats: TrafficStats) {
-//
-// }
-// })
-//// Core.startService()
- } catch (e: Exception) {
- Log.e(tag, "Error in startShadowsocks: $e")
- }
- } else {
- Log.e(tag, "Invalid config file!!")
- }
- } */
-
- private fun startOpenVpn() {
- if (isUp || mOpenVPNThreadv3 != null) {
- ovpnTurnOff()
- }
-
- mOpenVPNThreadv3 = OpenVPNThreadv3(this)
-
- Thread({
- mOpenVPNThreadv3?.run()
- }).start()
- }
-
- private fun startWireGuard(type: String) {
- val wireguard_conf = buildWireguardConfig(mConfig!!, type + "_config_data")
- if (currentTunnelHandle != -1) {
- Log.e(tag, "Tunnel already up")
- // Turn the tunnel down because this might be a switch
- GoBackend.wgTurnOff(currentTunnelHandle)
- }
- val wgConfig: String = wireguard_conf.toWgUserspaceString()
-
- val builder = Builder()
- setupBuilder(wireguard_conf, builder)
- builder.setSession("Amnezia")
-
-
- builder.establish().use { tun ->
- if (tun == null) return
- if (type == "awg"){
- currentTunnelHandle = GoBackend.wgTurnOn("awg0", tun.detachFd(), wgConfig)
- } else {
- currentTunnelHandle = GoBackend.wgTurnOn("amn0", tun.detachFd(), wgConfig)
- }
- }
- if (currentTunnelHandle < 0) {
- Log.e(tag, "Activation Error Code -> $currentTunnelHandle")
- isUp = false
+ val config = parseConfigToJson(vpnConfig)
+ if (config == null) {
+ onError("Invalid VPN config")
+ protocolState.value = DISCONNECTED
return
}
- protect(GoBackend.wgGetSocketV4(currentTunnelHandle))
- protect(GoBackend.wgGetSocketV6(currentTunnelHandle))
- isUp = true
- // Store the config in case the service gets
- // asked boot vpn from the OS
- val prefs = Prefs.get(this)
- prefs.edit()
- .putString("lastConf", mConfig.toString())
- .apply()
+ if (!checkPermission()) {
+ protocolState.value = DISCONNECTED
+ return
+ }
+
+ connectionJob = mainScope.launch {
+ disconnectionJob?.join()
+ disconnectionJob = null
+
+ protocol = getProtocol(config.getString("protocol"))
+ protocol?.let { protocol ->
+ protocol.initialize()
+ protocol.parseConfig(config)
+ protocol.startVpn(Builder(), ::protect)
+ }
+ protocolState.value = CONNECTED
+ }
}
- /* override suspend fun startProcesses() {
- worker = ProtectWorker().apply { start() }
+ @MainThread
+ private fun disconnect() {
+ Log.v(TAG, "Stop VPN connection")
+
+ if (isDisconnected || protocolState.value == DISCONNECTING) return
+
+ protocolState.value = DISCONNECTING
+
+ disconnectionJob = mainScope.launch {
+ connectionJob?.let {
+ if (it.isActive) it.cancelAndJoin()
+ }
+ connectionJob = null
+
+ protocol?.stopVpn()
+ protocol = null
+ protocolState.value = DISCONNECTED
+ }
+ }
+
+ private fun getProtocol(protocolName: String): Protocol =
+ when (protocolName) {
+ "wireguard" -> Wireguard(applicationContext)
+ else -> throw IllegalArgumentException("Failed to load $protocolName protocol")
+ }
+
+ /**
+ * Utils methods
+ */
+ @MainThread
+ private fun onError(msg: String) {
+ Log.e(TAG, msg)
+ clientMessenger.send {
+ ServiceEvent.ERROR.packToMessage {
+ putString(ERROR_MSG, msg)
+ }
+ }
+ }
+
+ private fun parseConfigToJson(vpnConfig: String?): JSONObject? =
try {
- Log.i(tag, "startProcesses: ------------------1")
- super.startProcesses()
- Log.i(tag, "startProcesses: ------------------2")
- sendFd(startVpn())
- Log.i(tag, "startProcesses: ------------------3")
- } catch (e: Exception) {
- e.printStackTrace()
- }
- } */
-
- /* override fun killProcesses(scope: CoroutineScope) {
- super.killProcesses(scope)
- active = false
- scope.launch { DefaultNetworkListener.stop(this) }
- worker?.shutdown(scope)
- worker = null
- conn?.close()
- conn = null
- } */
-
- /* private suspend fun startVpn(): FileDescriptor {
- val profile = data.proxy!!.profile
- Log.i(tag, "startVpn: -----------------------1")
- val builder = Builder()
- .setConfigureIntent(Core.configureIntent(this))
- .setSession(profile.formattedName)
- .setMtu(VPN_MTU)
- .addAddress(PRIVATE_VLAN4_CLIENT, 30)
- .addDnsServer(PRIVATE_VLAN4_ROUTER)
- Log.i(tag, "startVpn: -----------------------2")
- if (profile.ipv6) {
- builder.addAddress(PRIVATE_VLAN6_CLIENT, 126)
- builder.addRoute("::", 0)
- }
- Log.i(tag, "startVpn: -----------------------3")
- val me = packageName
- if (profile.proxyApps) {
- profile.individual.split('\n')
- .filter { it != me }
- .forEach {
- try {
- if (profile.bypass) builder.addDisallowedApplication(it)
- else builder.addAllowedApplication(it)
- } catch (ex: PackageManager.NameNotFoundException) {
- printLog(ex)
- }
- }
- if (profile.bypass) {
- builder.addDisallowedApplication(me)
+ vpnConfig?.let {
+ JSONObject(it)
}
+ } catch (e: JSONException) {
+ onError("Invalid VPN config json format: ${e.message}")
+ null
+ }
+
+ private fun checkPermission(): Boolean =
+ if (prepare(applicationContext) != null) {
+ Intent(this, VpnRequestActivity::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }.also {
+ startActivity(it)
+ }
+ false
} else {
- builder.addDisallowedApplication(me)
+ true
}
- Log.i(tag, "startVpn: -----------------------4")
- when (profile.route) {
- Acl.ALL, Acl.BYPASS_CHN, Acl.CUSTOM_RULES -> builder.addRoute("0.0.0.0", 0)
- else -> {
- resources.getStringArray(R.array.bypass_private_route).forEach {
- val subnet = Subnet.fromString(it)!!
- builder.addRoute(subnet.address.hostAddress, subnet.prefixSize)
- }
- builder.addRoute(PRIVATE_VLAN4_ROUTER, 32)
- }
- }
- Log.i(tag, "startVpn: -----------------------5")
- metered = profile.metered
- active = true // possible race condition here?
- Log.i(tag, "startVpn: -----------------------6")
- builder.setUnderlyingNetworks(underlyingNetworks)
- Log.i(tag, "startVpn: -----------------------7")
- val conn = builder.establish() ?: throw NullConnectionException()
- Log.i(tag, "startVpn: -----------------------8")
- this.conn = conn
- Log.i(tag, "startVpn: -----------------------9")
- val cmd = arrayListOf(
- File(applicationInfo.nativeLibraryDir, Executable.TUN2SOCKS).absolutePath,
- "--netif-ipaddr", PRIVATE_VLAN4_ROUTER,
- "--socks-server-addr", "${DataStore.listenAddress}:${DataStore.portProxy}",
- "--tunmtu", VPN_MTU.toString(),
- "--sock-path", "sock_path",
- "--dnsgw", "127.0.0.1:${DataStore.portLocalDns}",
- "--loglevel", "warning"
- )
- Log.i(tag, "startVpn: -----------------------10")
- if (profile.ipv6) {
- cmd += "--netif-ip6addr"
- cmd += PRIVATE_VLAN6_ROUTER
- }
- Log.i(tag, "startVpn: -----------------------11")
- cmd += "--enable-udprelay"
- Log.i(tag, "startVpn: -----------------------12")
- data.processes!!.start(cmd, onRestartCallback = {
- try {
- sendFd(conn.fileDescriptor)
- } catch (e: ErrnoException) {
- e.printStackTrace()
- stopRunner(false, e.message)
- }
- })
- Log.i(tag, "startVpn: -----------------------13")
- return conn.fileDescriptor
- } */
- /* private suspend fun sendFd(fd: FileDescriptor) {
- var tries = 0
- val path = File(Core.deviceStorage.noBackupFilesDir, "sock_path").absolutePath
- while (true) try {
- delay(50L shl tries)
- LocalSocket().use { localSocket ->
- localSocket.connect(
- LocalSocketAddress(
- path,
- LocalSocketAddress.Namespace.FILESYSTEM
- )
- )
- localSocket.setFileDescriptorsForSend(arrayOf(fd))
- localSocket.outputStream.write(42)
- }
- return
- } catch (e: IOException) {
- if (tries > 5) throw e
- tries += 1
- }
- } */
+ private fun loadConfigFromPrefs(): String? =
+ Prefs.get(this).getString(PREFS_CONFIG_KEY, null)
-
- /* private inner class ProtectWorker : ConcurrentLocalSocketListener(
- "ShadowsocksVpnThread",
- File(Core.deviceStorage.noBackupFilesDir, "protect_path")
- ) {
- override fun acceptInternal(socket: LocalSocket) {
- socket.inputStream.read()
- val fd = socket.ancillaryFileDescriptors!!.single()!!
- CloseableFd(fd).use {
- socket.outputStream.write(if (underlyingNetwork.let { network ->
- if (network != null && Build.VERSION.SDK_INT >= 23) try {
- network.bindSocket(fd)
- true
- } catch (e: IOException) {
- // suppress ENONET (Machine is not on the network)
- if ((e.cause as? ErrnoException)?.errno != 64) printLog(e)
- false
- } else protect(getInt.invoke(fd) as Int)
- }) 0 else 1)
- }
- }
- } */
-
- /* inner class NullConnectionException : NullPointerException() {
- override fun getLocalizedMessage() = getString(R.string.reboot_required)
- } */
-
- class CloseableFd(val fd: FileDescriptor) : Closeable {
- override fun close() = Os.close(fd)
- }
+ private fun saveConfigToPrefs(config: String?) =
+ Prefs.get(this).edit().putString(PREFS_CONFIG_KEY, config).apply()
}
diff --git a/client/android/src/org/amnezia/vpn/NetworkState.kt b/client/android/src/org/amnezia/vpn/NetworkState.kt
index 2ea573186..e1696fa92 100644
--- a/client/android/src/org/amnezia/vpn/NetworkState.kt
+++ b/client/android/src/org/amnezia/vpn/NetworkState.kt
@@ -153,7 +153,7 @@ class NetworkState(var service: AmneziaVpnService) {
defaultNetwork = NetworkTransports(network, newTransports)
}
if (capabilitiesChanged) {
- mService.networkChange()
+ // mService.networkChange()
Log.i(tag, "onCapabilitiesChanged capabilitiesChanged $network $networkCapabilities")
defaultNetworkCapabilities = newCapabilities
diff --git a/client/android/src/org/amnezia/vpn/NotificationUtil.kt b/client/android/src/org/amnezia/vpn/NotificationUtil.kt
deleted file mode 100644
index 541e27cd6..000000000
--- a/client/android/src/org/amnezia/vpn/NotificationUtil.kt
+++ /dev/null
@@ -1,115 +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.app.NotificationChannel
-import android.app.NotificationManager
-import android.app.PendingIntent
-import android.content.Context
-import android.content.Intent
-import android.os.Build
-import android.os.Parcel
-import androidx.core.app.NotificationCompat
-import org.json.JSONObject
-
-object NotificationUtil {
- var sCurrentContext: Context? = null
- private var sNotificationBuilder: NotificationCompat.Builder? = null
-
- const val NOTIFICATION_CHANNEL_ID = "com.amnezia.vpnNotification"
- const val CONNECTED_NOTIFICATION_ID = 1337
- const val tag = "NotificationUtil"
-
- /**
- * Updates the current shown notification from a
- * Parcel - Gets called from AndroidController.cpp
- */
- fun update(data: Parcel) {
- // [data] is here a json containing the notification content
- val buffer = data.createByteArray()
- val json = buffer?.let { String(it) }
- val content = JSONObject(json)
-
- update(content.getString("title"), content.getString("message"))
- }
-
- /**
- * Updates the current shown notification
- */
- fun update(heading: String, message: String) {
- if (sCurrentContext == null) return
- val notificationManager: NotificationManager =
- sCurrentContext?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
-
- sNotificationBuilder?.let {
- it.setContentTitle(heading)
- .setContentText(message)
- notificationManager.notify(CONNECTED_NOTIFICATION_ID, it.build())
- }
- }
-
- /**
- * Saves the default translated "connected" notification, in case the vpn gets started
- * without the app.
- */
- fun saveFallBackMessage(data: Parcel, context: Context) {
- // [data] is here a json containing the notification content
- val buffer = data.createByteArray()
- val json = buffer?.let { String(it) }
- val content = JSONObject(json)
-
- val prefs = Prefs.get(context)
- prefs.edit()
- .putString("fallbackNotificationHeader", content.getString("title"))
- .putString("fallbackNotificationMessage", content.getString("message"))
- .apply()
- Log.v(tag, "Saved new fallback message -> ${content.getString("title")}")
- }
-
- /*
- * Creates a new Notification using the current set of Strings
- * Shows the notification in the given {context}
- */
- fun show(service: AmneziaVpnService) {
- sNotificationBuilder = NotificationCompat.Builder(service, NOTIFICATION_CHANNEL_ID)
- sCurrentContext = service
- val notificationManager: NotificationManager =
- sCurrentContext?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- // From Oreo on we need to have a "notification channel" to post to.
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- val name = "vpn"
- val descriptionText = " "
- val importance = NotificationManager.IMPORTANCE_LOW
- val channel = NotificationChannel(NOTIFICATION_CHANNEL_ID, name, importance).apply {
- description = descriptionText
- }
- // Register the channel with the system
- notificationManager.createNotificationChannel(channel)
- }
- // In case we do not have gotten a message to show from the Frontend
- // try to populate the notification with a translated Fallback message
- val prefs = Prefs.get(service)
- val message =
- "" + prefs.getString("fallbackNotificationMessage", "Running in the Background")
- val header = "" + prefs.getString("fallbackNotificationHeader", "Amnezia VPN")
-
- // Create the Intent that Should be Fired if the User Clicks the notification
- val mainActivityName = "org.amnezia.vpn.AmneziaActivity"
- val activity = Class.forName(mainActivityName)
- val intent = Intent(service, activity)
- val pendingIntent = PendingIntent.getActivity(service, 0, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
- // Build our notification
- sNotificationBuilder?.let {
- it.setSmallIcon(R.drawable.ic_amnezia_round)
- .setContentTitle(header)
- .setContentText(message)
- .setOnlyAlertOnce(true)
- .setPriority(NotificationCompat.PRIORITY_DEFAULT)
- .setContentIntent(pendingIntent)
-
- service.startForeground(CONNECTED_NOTIFICATION_ID, it.build())
- }
- }
-}
diff --git a/client/android/src/org/amnezia/vpn/VPNServiceBinder.kt b/client/android/src/org/amnezia/vpn/VPNServiceBinder.kt
deleted file mode 100644
index 56777daf1..000000000
--- a/client/android/src/org/amnezia/vpn/VPNServiceBinder.kt
+++ /dev/null
@@ -1,211 +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.os.Binder
-import android.os.DeadObjectException
-import android.os.IBinder
-import android.os.Parcel
-import com.wireguard.config.*
-import org.json.JSONObject
-import java.lang.Exception
-
-class VPNServiceBinder(service: AmneziaVpnService) : Binder() {
-
- private val mService = service
- private val tag = "VPNServiceBinder"
- private var mListener: IBinder? = null
- private var mResumeConfig: JSONObject? = null
- private var mImportedConfig: String? = null
-
- /**
- * The codes this Binder does accept in [onTransact]
- */
- object ACTIONS {
- const val activate = 1
- const val deactivate = 2
- const val registerEventListener = 3
- const val requestStatistic = 4
- const val requestGetLog = 5
- const val requestCleanupLog = 6
- const val resumeActivate = 7
- const val setNotificationText = 8
- const val setFallBackNotification = 9
- const val importConfig = 11
- }
-
- /**
- * Gets called when the VPNServiceBinder gets a request from a Client.
- * The [code] determines what action is requested. - see [ACTIONS]
- * [data] may contain a utf-8 encoded json string with optional args or is null.
- * [reply] is a pointer to a buffer in the clients memory, to reply results.
- * we use this to send result data.
- *
- * returns true if the [code] was accepted
- */
- override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
- Log.i(tag, "GOT TRANSACTION " + code)
-
- when (code) {
- ACTIONS.activate -> {
- try {
- Log.i(tag, "Activation Requested, parsing Config")
- // [data] is here a json containing the wireguard/openvpn conf
- val buffer = data.createByteArray()
- val json = buffer?.let { String(it) }
- val config = JSONObject(json)
- Log.v(tag, "Stored new Tunnel config in Service")
- Log.i(tag, "Config: $config")
- if (!mService.checkPermissions()) {
- mResumeConfig = config
- // The Permission prompt was already
- // send, in case it's accepted we will
- // receive ACTIONS.resumeActivate
- return true
- }
- this.mService.turnOn(config)
- } catch (e: Exception) {
- Log.e(tag, "An Error occurred while enabling the VPN: ${e.localizedMessage}")
- dispatchEvent(EVENTS.activationError, e.localizedMessage)
- }
- return true
- }
-
- ACTIONS.resumeActivate -> {
- // [data] is empty
- // Activate the current tunnel
- Log.i(tag, "resume activate")
- try {
- mResumeConfig?.let { this.mService.turnOn(it) }
- } catch (e: Exception) {
- Log.e(tag, "An Error occurred while enabling the VPN: ${e.localizedMessage}")
- }
- return true
- }
-
- ACTIONS.deactivate -> {
- // [data] here is empty
- this.mService.turnOff()
- return true
- }
-
- ACTIONS.registerEventListener -> {
- Log.i(tag, "register: start")
- // [data] contains the Binder that we need to dispatch the Events
- val binder = data.readStrongBinder()
- mListener = binder
- val obj = JSONObject()
- obj.put("connected", mService.isUp)
- obj.put("time", mService.connectionTime)
- dispatchEvent(EVENTS.init, obj.toString())
-
- ////
- if (mImportedConfig != null) {
- Log.i(tag, "register: config not null")
- dispatchEvent(EVENTS.configImport, mImportedConfig)
- mImportedConfig = null
- } else {
- Log.i(tag, "register: config is null")
- }
-
- return true
- }
-
- ACTIONS.requestStatistic -> {
- dispatchEvent(EVENTS.statisticUpdate, mService.status.toString())
- return true
- }
-
- ACTIONS.requestGetLog -> {
- // Grabs all the Logs and dispatch new Log Event
- // dispatchEvent(EVENTS.backendLogs, Log.getContent())
- return true
- }
-
- ACTIONS.requestCleanupLog -> {
- // Log.clearFile()
- return true
- }
-
- ACTIONS.setNotificationText -> {
- NotificationUtil.update(data)
- return true
- }
-
- ACTIONS.setFallBackNotification -> {
- NotificationUtil.saveFallBackMessage(data, mService)
- return true
- }
-
- ACTIONS.importConfig -> {
- val buffer = data.readString()
-
- val obj = JSONObject()
- obj.put("config", buffer)
-
- val resultString = obj.toString()
-
- Log.i(tag, "Transact import config request")
-
- if (mListener != null) {
- dispatchEvent(EVENTS.configImport, resultString)
- } else {
- mImportedConfig = resultString
- }
- }
-
- IBinder.LAST_CALL_TRANSACTION -> {
- Log.e(tag, "The OS Requested to shut down the VPN")
- this.mService.turnOff()
- return true
- }
-
- else -> {
- Log.e(tag, "Received invalid bind request \t Code -> $code")
- // If we're hitting this there is probably something wrong in the client.
- return false
- }
- }
-
- return false
- }
-
- /**
- * Dispatches an Event to all registered Binders
- * [code] the Event that happened - see [EVENTS]
- * To register an Eventhandler use [onTransact] with
- * [ACTIONS.registerEventListener]
- */
- fun dispatchEvent(code: Int, payload: String?) {
- try {
- mListener?.let {
- if (it.isBinderAlive) {
- val data = Parcel.obtain()
- data.writeByteArray(payload?.toByteArray(charset("UTF-8")))
- it.transact(code, data, Parcel.obtain(), 0)
- } else {
- Log.i(tag, "Dispatching event: binder NOT alive")
- }
- }
- } catch (e: DeadObjectException) {
- // If the QT Process is killed (not just inactive)
- // we cant access isBinderAlive, so nothing to do here.
- }
- }
-
- /**
- * The codes we Are Using in case of [dispatchEvent]
- * Qt codes in the androidvpnactivity.h
- */
- object EVENTS {
- const val init = 0
- const val connected = 1
- const val disconnected = 2
- const val statisticUpdate = 3
- const val backendLogs = 4
- const val activationError = 5
- const val permissionRequired = 6
- const val configImport = 7
- }
-}
diff --git a/client/android/src/org/amnezia/vpn/VpnRequestActivity.kt b/client/android/src/org/amnezia/vpn/VpnRequestActivity.kt
new file mode 100644
index 000000000..4fa878e10
--- /dev/null
+++ b/client/android/src/org/amnezia/vpn/VpnRequestActivity.kt
@@ -0,0 +1,69 @@
+package org.amnezia.vpn
+
+import android.app.KeyguardManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.net.VpnService
+import android.os.Bundle
+import android.widget.Toast
+import androidx.activity.ComponentActivity
+import androidx.activity.result.ActivityResult
+import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
+import androidx.core.content.ContextCompat
+import androidx.core.content.getSystemService
+
+private const val TAG = "VpnRequestActivity"
+
+class VpnRequestActivity : ComponentActivity() {
+
+ private var userPresentReceiver: BroadcastReceiver? = null
+ private val requestLauncher =
+ registerForActivityResult(StartActivityForResult(), ::checkRequestResult)
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ Log.v(TAG, "Start request activity")
+ val requestIntent = VpnService.prepare(applicationContext)
+ if (requestIntent != null) {
+ if (getSystemService()!!.isKeyguardLocked) {
+ userPresentReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) =
+ requestLauncher.launch(requestIntent)
+ }
+ registerReceiver(userPresentReceiver, IntentFilter(Intent.ACTION_USER_PRESENT))
+ } else {
+ requestLauncher.launch(requestIntent)
+ }
+ return
+ } else {
+ onPermissionGranted()
+ finish()
+ }
+ }
+
+ override fun onDestroy() {
+ userPresentReceiver?.let {
+ unregisterReceiver(it)
+ }
+ super.onDestroy()
+ }
+
+ private fun checkRequestResult(result: ActivityResult) {
+ when (result.resultCode) {
+ RESULT_OK -> onPermissionGranted()
+ else -> Toast.makeText(this, "Vpn permission denied", Toast.LENGTH_LONG).show()
+ }
+ finish()
+ }
+
+ private fun onPermissionGranted() {
+ Toast.makeText(this, "Vpn permission granted", Toast.LENGTH_LONG).show()
+ Intent(applicationContext, AmneziaVpnService::class.java).apply {
+ putExtra(AFTER_PERMISSION_CHECK, true)
+ }.also {
+ ContextCompat.startForegroundService(this, it)
+ }
+ }
+}