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:taskAffinity=""
android:exported="false" /> android:exported="false" />
<activity
android:name=".VpnRequestActivity"
android:excludeFromRecents="true"
android:launchMode="singleTask"
android:taskAffinity=""
android:exported="false"
android:theme="@style/Translucent" />
<activity <activity
android:name=".ImportConfigActivity" android:name=".ImportConfigActivity"
android:exported="true"> android:exported="true">

View file

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

View file

@ -4,4 +4,14 @@
<item name="android:windowActionBar">false</item> <item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item> <item name="android:windowNoTitle">true</item>
</style> </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> </resources>

View file

@ -6,13 +6,11 @@ import android.content.ServiceConnection
import android.net.Uri import android.net.Uri
import android.net.VpnService import android.net.VpnService
import android.os.Bundle import android.os.Bundle
import android.os.DeadObjectException
import android.os.Handler import android.os.Handler
import android.os.IBinder import android.os.IBinder
import android.os.Looper import android.os.Looper
import android.os.Message import android.os.Message
import android.os.Messenger import android.os.Messenger
import android.os.RemoteException
import android.widget.Toast import android.widget.Toast
import androidx.annotation.MainThread import androidx.annotation.MainThread
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -76,6 +74,9 @@ class AmneziaActivity : QtActivity() {
} }
ServiceEvent.ERROR -> { ServiceEvent.ERROR -> {
msg.data?.getString(ERROR_MSG)?.let { error ->
Log.e(TAG, "From VpnService: $error")
}
// todo: add error reporting to Qt // todo: add error reporting to Qt
QtAndroidController.onServiceError() QtAndroidController.onServiceError()
} }

View file

@ -3,14 +3,32 @@ package org.amnezia.vpn
import androidx.camera.camera2.Camera2Config import androidx.camera.camera2.Camera2Config
import androidx.camera.core.CameraSelector import androidx.camera.core.CameraSelector
import androidx.camera.core.CameraXConfig import androidx.camera.core.CameraXConfig
import androidx.core.app.NotificationChannelCompat.Builder
import androidx.core.app.NotificationManagerCompat
import org.qtproject.qt.android.bindings.QtApplication import org.qtproject.qt.android.bindings.QtApplication
const val NOTIFICATION_CHANNEL_ID: String = "org.amnezia.vpn.notification"
class AmneziaApplication : QtApplication(), CameraXConfig.Provider { class AmneziaApplication : QtApplication(), CameraXConfig.Provider {
override fun onCreate() {
super.onCreate()
createNotificationChannel()
}
override fun getCameraXConfig(): CameraXConfig = CameraXConfig.Builder override fun getCameraXConfig(): CameraXConfig = CameraXConfig.Builder
.fromConfig(Camera2Config.defaultConfig()) .fromConfig(Camera2Config.defaultConfig())
.setMinimumLoggingLevel(android.util.Log.ERROR) .setMinimumLoggingLevel(android.util.Log.ERROR)
.setAvailableCamerasLimiter(CameraSelector.DEFAULT_BACK_CAMERA) .setAvailableCamerasLimiter(CameraSelector.DEFAULT_BACK_CAMERA)
.build() .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) defaultNetwork = NetworkTransports(network, newTransports)
} }
if (capabilitiesChanged) { if (capabilitiesChanged) {
mService.networkChange() // mService.networkChange()
Log.i(tag, "onCapabilitiesChanged capabilitiesChanged $network $networkCapabilities") Log.i(tag, "onCapabilitiesChanged capabilitiesChanged $network $networkCapabilities")
defaultNetworkCapabilities = newCapabilities 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)
}
}
}