Vpn service refactoring
This commit is contained in:
parent
8ef16781eb
commit
385a52f676
10 changed files with 408 additions and 1262 deletions
|
@ -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">
|
||||
|
|
|
@ -4,6 +4,5 @@ enum class ProtocolState {
|
|||
CONNECTED,
|
||||
CONNECTING,
|
||||
DISCONNECTED,
|
||||
DISCONNECTING,
|
||||
UNKNOWN
|
||||
DISCONNECTING
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
69
client/android/src/org/amnezia/vpn/VpnRequestActivity.kt
Normal file
69
client/android/src/org/amnezia/vpn/VpnRequestActivity.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue