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.
216 lines
7.1 KiB
Kotlin
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"
|
|
}
|
|
}
|