Files
amnezia-client/client/android/src/org/amnezia/vpn/AmneziaVpnService.kt
T

632 lines
22 KiB
Kotlin

package org.amnezia.vpn
import android.annotation.SuppressLint
import android.app.ActivityManager
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE
import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED
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 android.os.PowerManager
import android.os.Process
import androidx.annotation.MainThread
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import java.net.UnknownHostException
import java.util.concurrent.ConcurrentHashMap
import kotlin.LazyThreadSafetyMode.NONE
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.cancel
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import org.amnezia.vpn.protocol.BadConfigException
import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
import org.amnezia.vpn.protocol.ProtocolState.CONNECTING
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTING
import org.amnezia.vpn.protocol.ProtocolState.RECONNECTING
import org.amnezia.vpn.protocol.ProtocolState.UNKNOWN
import org.amnezia.vpn.protocol.VpnException
import org.amnezia.vpn.protocol.VpnStartException
import org.amnezia.vpn.protocol.putStatus
import org.amnezia.vpn.util.LoadLibraryException
import org.amnezia.vpn.util.Log
import org.amnezia.vpn.util.Prefs
import org.amnezia.vpn.util.net.NetworkState
import org.amnezia.vpn.util.net.TrafficStats
import org.json.JSONException
import org.json.JSONObject
private const val TAG = "AmneziaVpnService"
const val ACTION_DISCONNECT = "org.amnezia.vpn.action.disconnect"
const val ACTION_CONNECT = "org.amnezia.vpn.action.connect"
const val MSG_VPN_CONFIG = "VPN_CONFIG"
const val MSG_ERROR = "ERROR"
const val MSG_SAVE_LOGS = "SAVE_LOGS"
const val MSG_CLIENT_NAME = "CLIENT_NAME"
const val AFTER_PERMISSION_CHECK = "AFTER_PERMISSION_CHECK"
private const val PREFS_CONFIG_KEY = "LAST_CONF"
private const val PREFS_SERVER_NAME = "LAST_SERVER_NAME"
private const val PREFS_SERVER_INDEX = "LAST_SERVER_INDEX"
// private const val STATISTICS_SENDING_TIMEOUT = 1000L
private const val TRAFFIC_STATS_UPDATE_TIMEOUT = 1000L
private const val DISCONNECT_TIMEOUT = 5000L
private const val STOP_SERVICE_TIMEOUT = 5000L
@SuppressLint("Registered")
open class AmneziaVpnService : VpnService() {
private lateinit var mainScope: CoroutineScope
private lateinit var connectionScope: CoroutineScope
private var isServiceBound = false
private var vpnProto: VpnProto? = null
private var protocolState = MutableStateFlow(UNKNOWN)
private var serverName: String? = null
private var serverIndex: Int = -1
private val isConnected
get() = protocolState.value == CONNECTED
private val isDisconnected
get() = protocolState.value == DISCONNECTED
private val isUnknown
get() = protocolState.value == UNKNOWN
private var connectionJob: Job? = null
private var disconnectionJob: Job? = null
private var trafficStatsUpdateJob: Job? = null
// private var statisticsSendingJob: Job? = null
private lateinit var networkState: NetworkState
private lateinit var trafficStats: TrafficStats
private var controlReceiver: BroadcastReceiver? = null
private var notificationStateReceiver: BroadcastReceiver? = null
private var screenOnReceiver: BroadcastReceiver? = null
private var screenOffReceiver: BroadcastReceiver? = null
private val clientMessengers = ConcurrentHashMap<Messenger, IpcMessenger>()
private val isActivityConnected
get() = clientMessengers.any { it.value.name == ACTIVITY_MESSENGER_NAME }
private val connectionExceptionHandler = CoroutineExceptionHandler { _, e ->
connectionJob?.cancel()
connectionJob = null
disconnectionJob?.cancel()
disconnectionJob = null
protocolState.value = DISCONNECTED
when (e) {
is IllegalArgumentException,
is VpnStartException,
is VpnException -> 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}")
is UnknownHostException -> onError("Unknown host")
else -> throw e
}
}
private val actionMessageHandler: Handler by lazy(NONE) {
object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
val action = msg.extractIpcMessage<Action>()
Log.d(TAG, "Handle action: $action")
when (action) {
Action.REGISTER_CLIENT -> {
val clientName = msg.data.getString(MSG_CLIENT_NAME)
val messenger = IpcMessenger(msg.replyTo, clientName)
clientMessengers[msg.replyTo] = messenger
Log.d(TAG, "Messenger client '$clientName' was registered")
// if (clientName == ACTIVITY_MESSENGER_NAME && isConnected) launchSendingStatistics()
}
Action.UNREGISTER_CLIENT -> {
clientMessengers.remove(msg.replyTo)?.let {
Log.d(TAG, "Messenger client '${it.name}' was unregistered")
// if (it.name == ACTIVITY_MESSENGER_NAME) stopSendingStatistics()
}
}
Action.CONNECT -> {
connect(msg.data.getString(MSG_VPN_CONFIG))
}
Action.DISCONNECT -> {
disconnect()
}
Action.REQUEST_STATUS -> {
clientMessengers[msg.replyTo]?.let { clientMessenger ->
clientMessenger.send {
ServiceEvent.STATUS.packToMessage {
putStatus(this@AmneziaVpnService.protocolState.value)
}
}
}
}
Action.NOTIFICATION_PERMISSION_GRANTED -> {
enableNotification()
}
Action.SET_SAVE_LOGS -> {
Log.saveLogs = msg.data.getBoolean(MSG_SAVE_LOGS)
}
}
}
}
}
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_SYSTEM_EXEMPTED
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> FOREGROUND_SERVICE_TYPE_MANIFEST
else -> 0
}
private val serviceNotification: ServiceNotification by lazy(NONE) { ServiceNotification(this) }
/**
* Service overloaded methods
*/
override fun onCreate() {
super.onCreate()
Log.d(TAG, "Create Amnezia VPN service")
mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
connectionScope = CoroutineScope(SupervisorJob() + Dispatchers.IO + connectionExceptionHandler)
loadServerData()
launchProtocolStateHandler()
networkState = NetworkState(this, ::reconnect)
trafficStats = TrafficStats()
registerBroadcastReceivers()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val isAlwaysOn = intent != null && intent.action == SERVICE_INTERFACE
if (isAlwaysOn) {
Log.d(TAG, "Start service via Always-on")
connect()
} else if (intent?.getBooleanExtra(AFTER_PERMISSION_CHECK, false) == true) {
Log.d(TAG, "Start service after permission check")
connect()
} else {
Log.d(TAG, "Start service")
connect(intent?.getStringExtra(MSG_VPN_CONFIG))
}
ServiceCompat.startForeground(
this, NOTIFICATION_ID,
serviceNotification.buildNotification(serverName, vpnProto?.label, protocolState.value),
foregroundServiceTypeCompat
)
return START_REDELIVER_INTENT
}
override fun onBind(intent: Intent?): IBinder? {
Log.d(TAG, "onBind by $intent")
if (intent?.action == SERVICE_INTERFACE) return super.onBind(intent)
isServiceBound = true
return vpnServiceMessenger.binder
}
override fun onUnbind(intent: Intent?): Boolean {
Log.d(TAG, "onUnbind by $intent")
if (intent?.action != SERVICE_INTERFACE) {
if (clientMessengers.isEmpty()) {
isServiceBound = false
if (isUnknown || isDisconnected) stopService()
}
}
return true
}
override fun onRebind(intent: Intent?) {
Log.d(TAG, "onRebind by $intent")
if (intent?.action != SERVICE_INTERFACE) {
isServiceBound = true
}
super.onRebind(intent)
}
override fun onRevoke() {
Log.d(TAG, "onRevoke")
// Calls to onRevoke() method may not happen on the main thread of the process
mainScope.launch {
disconnect()
}
}
override fun onDestroy() {
Log.d(TAG, "Destroy service")
unregisterBroadcastReceivers()
runBlocking {
disconnect()
disconnectionJob?.join()
}
connectionScope.cancel()
mainScope.cancel()
super.onDestroy()
}
private fun stopService() {
Log.d(TAG, "Stop service")
// the coroutine below will be canceled during the onDestroy call
mainScope.launch {
delay(STOP_SERVICE_TIMEOUT)
Log.w(TAG, "Stop service timeout, kill process")
Process.killProcess(Process.myPid())
}
stopSelf()
}
private fun registerBroadcastReceivers() {
Log.d(TAG, "Register broadcast receivers")
controlReceiver = registerBroadcastReceiver(
arrayOf(ACTION_CONNECT, ACTION_DISCONNECT), ContextCompat.RECEIVER_NOT_EXPORTED
) {
it?.action?.let { action ->
Log.v(TAG, "Broadcast request received: $action")
when (action) {
ACTION_CONNECT -> connect()
ACTION_DISCONNECT -> disconnect()
else -> Log.w(TAG, "Unknown action received: $action")
}
}
}
notificationStateReceiver = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
registerBroadcastReceiver(
arrayOf(
NotificationManager.ACTION_NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED,
NotificationManager.ACTION_APP_BLOCK_STATE_CHANGED
)
) {
val state = it?.getBooleanExtra(NotificationManager.EXTRA_BLOCKED_STATE, false)
Log.v(TAG, "Notification state changed: ${it?.action}, blocked = $state")
if (state == false) {
enableNotification()
} else {
disableNotification()
}
}
} else null
registerScreenStateBroadcastReceivers()
}
private fun registerScreenStateBroadcastReceivers() {
if (serviceNotification.isNotificationEnabled()) {
Log.d(TAG, "Register screen state broadcast receivers")
screenOnReceiver = registerBroadcastReceiver(Intent.ACTION_SCREEN_ON) {
if (isConnected && serviceNotification.isNotificationEnabled()) startTrafficStatsUpdateJob()
}
screenOffReceiver = registerBroadcastReceiver(Intent.ACTION_SCREEN_OFF) {
stopTrafficStatsUpdateJob()
}
}
}
private fun unregisterScreenStateBroadcastReceivers() {
Log.d(TAG, "Unregister screen state broadcast receivers")
unregisterBroadcastReceiver(screenOnReceiver)
unregisterBroadcastReceiver(screenOffReceiver)
screenOnReceiver = null
screenOffReceiver = null
}
private fun unregisterBroadcastReceivers() {
Log.d(TAG, "Unregister broadcast receivers")
unregisterBroadcastReceiver(controlReceiver)
unregisterBroadcastReceiver(notificationStateReceiver)
unregisterScreenStateBroadcastReceivers()
controlReceiver = null
notificationStateReceiver = null
}
/**
* Methods responsible for processing VPN connection
*/
private fun launchProtocolStateHandler() {
mainScope.launch {
// drop first default UNKNOWN state
protocolState.drop(1).collect { protocolState ->
Log.d(TAG, "Protocol state changed: $protocolState")
serviceNotification.updateNotification(serverName, vpnProto?.label, protocolState)
clientMessengers.send {
ServiceEvent.STATUS_CHANGED.packToMessage {
putStatus(protocolState)
}
}
VpnStateStore.store { VpnState(protocolState, serverName, serverIndex, vpnProto) }
when (protocolState) {
CONNECTED -> {
networkState.bindNetworkListener()
// if (isActivityConnected) launchSendingStatistics()
launchTrafficStatsUpdate()
}
DISCONNECTED -> {
networkState.unbindNetworkListener()
stopTrafficStatsUpdateJob()
// stopSendingStatistics()
if (!isServiceBound) stopService()
}
DISCONNECTING -> {
networkState.unbindNetworkListener()
stopTrafficStatsUpdateJob()
// stopSendingStatistics()
}
RECONNECTING -> {
stopTrafficStatsUpdateJob()
// stopSendingStatistics()
}
CONNECTING, UNKNOWN -> {}
}
}
}
}
/* @MainThread
private fun launchSendingStatistics() {
if (isServiceBound && isConnected) {
statisticsSendingJob = mainScope.launch {
while (true) {
clientMessenger.send {
ServiceEvent.STATISTICS_UPDATE.packToMessage {
putStatistics(protocol?.statistics ?: Statistics.EMPTY_STATISTICS)
}
}
delay(STATISTICS_SENDING_TIMEOUT)
}
}
}
}
@MainThread
private fun stopSendingStatistics() {
statisticsSendingJob?.cancel()
} */
@MainThread
private fun enableNotification() {
registerScreenStateBroadcastReceivers()
serviceNotification.updateNotification(serverName, vpnProto?.label, protocolState.value)
launchTrafficStatsUpdate()
}
@MainThread
private fun disableNotification() {
unregisterScreenStateBroadcastReceivers()
stopTrafficStatsUpdateJob()
}
@MainThread
private fun launchTrafficStatsUpdate() {
stopTrafficStatsUpdateJob()
if (isConnected &&
serviceNotification.isNotificationEnabled() &&
getSystemService<PowerManager>()?.isInteractive != false
) {
Log.v(TAG, "Launch traffic stats update")
trafficStats.reset()
startTrafficStatsUpdateJob()
}
}
@MainThread
private fun startTrafficStatsUpdateJob() {
if (trafficStatsUpdateJob == null && trafficStats.isSupported()) {
Log.d(TAG, "Start traffic stats update")
trafficStatsUpdateJob = mainScope.launch {
while (true) {
trafficStats.getSpeed().let { speed ->
if (isConnected) {
serviceNotification.updateSpeed(speed)
}
}
delay(TRAFFIC_STATS_UPDATE_TIMEOUT)
}
}
}
}
@MainThread
private fun stopTrafficStatsUpdateJob() {
Log.d(TAG, "Stop traffic stats update")
trafficStatsUpdateJob?.cancel()
trafficStatsUpdateJob = null
}
@MainThread
private fun connect(vpnConfig: String? = null) {
if (vpnConfig == null) {
connectToVpn(Prefs.load(PREFS_CONFIG_KEY))
} else {
Prefs.save(PREFS_CONFIG_KEY, vpnConfig)
connectToVpn(vpnConfig)
}
}
@MainThread
private fun connectToVpn(vpnConfig: String) {
if (isConnected || protocolState.value == CONNECTING) return
Log.d(TAG, "Start VPN connection")
val config = parseConfigToJson(vpnConfig)
saveServerData(config)
if (config == null) {
onError("Invalid VPN config")
protocolState.value = DISCONNECTED
return
}
try {
vpnProto = VpnProto.get(config.getString("protocol"))
} catch (e: Exception) {
onError("Invalid VPN config: ${e.message}")
protocolState.value = DISCONNECTED
return
}
protocolState.value = CONNECTING
if (!checkPermission()) {
protocolState.value = DISCONNECTED
return
}
connectionJob = connectionScope.launch {
disconnectionJob?.join()
disconnectionJob = null
vpnProto?.protocol?.let { protocol ->
protocol.initialize(applicationContext, protocolState, ::onError)
protocol.startVpn(config, Builder(), ::protect)
}
}
}
@MainThread
private fun disconnect() {
if (isUnknown || isDisconnected || protocolState.value == DISCONNECTING) return
Log.d(TAG, "Stop VPN connection")
protocolState.value = DISCONNECTING
disconnectionJob = connectionScope.launch {
connectionJob?.cancelAndJoin()
connectionJob = null
vpnProto?.protocol?.stopVpn()
try {
withTimeout(DISCONNECT_TIMEOUT) {
// waiting for disconnect state
protocolState.first { it == DISCONNECTED }
}
} catch (e: TimeoutCancellationException) {
Log.w(TAG, "Disconnect timeout")
stopService()
}
}
}
@MainThread
private fun reconnect() {
if (!isConnected) return
Log.d(TAG, "Reconnect VPN")
protocolState.value = RECONNECTING
connectionJob = connectionScope.launch {
vpnProto?.protocol?.reconnectVpn(Builder(), ::protect)
}
}
/**
* Utils methods
*/
private fun onError(msg: String) {
Log.e(TAG, msg)
mainScope.launch {
clientMessengers.send {
ServiceEvent.ERROR.packToMessage {
putString(MSG_ERROR, msg)
}
}
}
}
private fun parseConfigToJson(vpnConfig: String): JSONObject? =
if (vpnConfig.isBlank()) {
null
} else {
try {
JSONObject(vpnConfig)
} catch (e: JSONException) {
onError("Invalid VPN config json format: ${e.message}")
null
}
}
private fun saveServerData(config: JSONObject?) {
serverName = config?.opt("description") as String?
serverIndex = config?.opt("serverIndex") as Int? ?: -1
Log.d(TAG, "Save server data: ($serverIndex, $serverName)")
Prefs.save(PREFS_SERVER_NAME, serverName)
Prefs.save(PREFS_SERVER_INDEX, serverIndex)
}
private fun loadServerData() {
serverName = Prefs.load<String>(PREFS_SERVER_NAME).ifBlank { null }
if (serverName != null) serverIndex = Prefs.load(PREFS_SERVER_INDEX)
Log.d(TAG, "Load server data: ($serverIndex, $serverName)")
}
private fun checkPermission(): Boolean =
if (prepare(applicationContext) != null) {
Intent(this, VpnRequestActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
putExtra(EXTRA_PROTOCOL, vpnProto)
}.also {
startActivity(it)
}
false
} else {
true
}
companion object {
fun isRunning(context: Context, processName: String): Boolean =
context.getSystemService<ActivityManager>()!!.runningAppProcesses.any {
it.processName == processName && it.importance <= IMPORTANCE_FOREGROUND_SERVICE
}
}
}