Vpn service refactoring

This commit is contained in:
albexk 2023-11-24 21:51:09 +03:00
parent 8ef16781eb
commit 385a52f676
10 changed files with 408 additions and 1262 deletions

View file

@ -68,6 +68,14 @@
android:taskAffinity=""
android:exported="false" />
<activity
android:name=".VpnRequestActivity"
android:excludeFromRecents="true"
android:launchMode="singleTask"
android:taskAffinity=""
android:exported="false"
android:theme="@style/Translucent" />
<activity
android:name=".ImportConfigActivity"
android:exported="true">

View file

@ -4,6 +4,5 @@ enum class ProtocolState {
CONNECTED,
CONNECTING,
DISCONNECTED,
DISCONNECTING,
UNKNOWN
DISCONNECTING
}

View file

@ -4,4 +4,14 @@
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
</style>
<style name="Translucent" parent="NoActionBar">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowFrame">@null</item>
<item name="android:windowIsFloating">true</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowAnimationStyle">@null</item>
<item name="android:backgroundDimEnabled">false</item>
<item name="android:windowCloseOnTouchOutside">false</item>
</style>
</resources>

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

@ -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<KeyguardManager>()!!.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)
}
}
}