You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tailscale-android/android/src/main/java/com/tailscale/ipn/IPNService.kt

216 lines
7.1 KiB
Kotlin

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package com.tailscale.ipn
import android.app.PendingIntent
import android.content.Intent
import android.content.pm.PackageManager
import android.net.VpnService
import android.os.Build
import android.system.OsConstants
import com.tailscale.ipn.mdm.MDMSettings
import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.notifier.Notifier
import com.tailscale.ipn.util.TSLog
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import libtailscale.Libtailscale
import java.util.UUID
open class IPNService : VpnService(), libtailscale.IPNService {
private val TAG = "IPNService"
private val randomID: String = UUID.randomUUID().toString()
private lateinit var app: App
val scope = CoroutineScope(Dispatchers.IO)
override fun id(): String {
return randomID
}
override fun updateVpnStatus(status: Boolean) {
app.getAppScopedViewModel().setVpnActive(status)
}
override fun onCreate() {
super.onCreate()
// grab app to make sure it initializes
app = App.get()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int =
when (intent?.action) {
ACTION_STOP_VPN -> {
app.setWantRunning(false)
close()
START_NOT_STICKY
}
ACTION_START_VPN -> {
scope.launch {
// Collect the first value of hideDisconnectAction asynchronously.
val hideDisconnectAction = MDMSettings.forceEnabled.flow.first()
showForegroundNotification(hideDisconnectAction.value)
}
app.setWantRunning(true)
Libtailscale.requestVPN(this)
START_STICKY
}
"android.net.VpnService" -> {
// This means we were started by Android due to Always On VPN.
// We show a non-foreground notification because we weren't
// started as a foreground service.
scope.launch {
// Collect the first value of hideDisconnectAction asynchronously.
val hideDisconnectAction = MDMSettings.forceEnabled.flow.first()
app.notifyStatus(true, hideDisconnectAction.value)
}
app.setWantRunning(true)
Libtailscale.requestVPN(this)
START_STICKY
}
else -> {
// This means that we were restarted after the service was killed
// (potentially due to OOM).
if (UninitializedApp.get().isAbleToStartVPN()) {
scope.launch {
// Collect the first value of hideDisconnectAction asynchronously.
val hideDisconnectAction = MDMSettings.forceEnabled.flow.first()
showForegroundNotification(hideDisconnectAction.value)
}
App.get()
Libtailscale.requestVPN(this)
START_STICKY
} else {
START_NOT_STICKY
}
}
}
override fun close() {
app.setWantRunning(false) {}
Notifier.setState(Ipn.State.Stopping)
disconnectVPN()
Libtailscale.serviceDisconnect(this)
}
override fun disconnectVPN() {
stopSelf()
}
override fun onDestroy() {
close()
updateVpnStatus(false)
super.onDestroy()
}
override fun onRevoke() {
close()
updateVpnStatus(false)
super.onRevoke()
}
private fun setVpnPrepared(isPrepared: Boolean) {
app.getAppScopedViewModel().setVpnPrepared(isPrepared)
}
private fun showForegroundNotification(hideDisconnectAction: Boolean) {
try {
startForeground(
UninitializedApp.STATUS_NOTIFICATION_ID,
UninitializedApp.get().buildStatusNotification(true, hideDisconnectAction))
} catch (e: Exception) {
TSLog.e(TAG, "Failed to start foreground service: $e")
}
}
private fun configIntent(): PendingIntent {
return PendingIntent.getActivity(
this,
0,
Intent(this, MainActivity::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
}
private fun allowApp(b: Builder, name: String) {
try {
b.addAllowedApplication(name)
} catch (e: PackageManager.NameNotFoundException) {
TSLog.e(TAG, "Failed to add allowed application: $e")
}
}
private fun disallowApp(b: Builder, name: String) {
try {
b.addDisallowedApplication(name)
} catch (e: PackageManager.NameNotFoundException) {
TSLog.e(TAG, "Failed to add disallowed application: $e")
}
}
override fun newBuilder(): VPNServiceBuilder {
val b: Builder =
Builder()
.setConfigureIntent(configIntent())
.allowFamily(OsConstants.AF_INET)
.allowFamily(OsConstants.AF_INET6)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
b.setMetered(false) // Inherit the metered status from the underlying networks.
}
b.setUnderlyingNetworks(null) // Use all available networks.
val mdmAllowed =
MDMSettings.includedPackages.flow.value.value?.split(",")?.map { it.trim() } ?: emptyList()
val mdmDisallowed =
MDMSettings.excludedPackages.flow.value.value?.split(",")?.map { it.trim() } ?: emptyList()
var packagesList: List<String>
var allowPackages: Boolean
if (mdmAllowed.isNotEmpty()) {
// An admin defined a list of packages that are exclusively allowed to be used via
// Tailscale, so only allow those.
packagesList = mdmAllowed
allowPackages = true
TSLog.d(TAG, "Included application packages were set via MDM: $mdmAllowed")
} else if (mdmDisallowed.isNotEmpty()) {
// An admin defined a list of packages that are excluded from accessing Tailscale,
// so ignore user definitions and only exclude those
packagesList = mdmDisallowed
allowPackages = false
TSLog.d(TAG, "Excluded application packages were set via MDM: $mdmDisallowed")
} else {
// Otherwise, prevent user manually disallowed apps from getting their traffic + DNS routed
// via Tailscale
packagesList = UninitializedApp.get().selectedPackageNames()
allowPackages = UninitializedApp.get().allowSelectedPackages()
TSLog.d(TAG, "Application packages were set by user: $packagesList")
}
if (allowPackages) {
// There always needs to be at least one allowed application for the VPN service to filter the
// traffic so add our own application by default to fulfill that requirement
packagesList += BuildConfig.APPLICATION_ID
for (packageName in packagesList) {
TSLog.d(TAG, "Including app: $packageName")
allowApp(b, packageName)
}
} else {
// Make sure to also exclude hard-coded apps that are known to cause issues
packagesList += UninitializedApp.get().builtInDisallowedPackageNames
for (packageName in packagesList) {
TSLog.d(TAG, "Disallowing app: $packageName")
disallowApp(b, packageName)
}
}
return VPNServiceBuilder(b)
}
companion object {
const val ACTION_START_VPN = "com.tailscale.ipn.START_VPN"
const val ACTION_STOP_VPN = "com.tailscale.ipn.STOP_VPN"
}
}