// 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 androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import libtailscale.Libtailscale import java.util.UUID open class IPNService : VpnService(), libtailscale.IPNService { private val randomID: String = UUID.randomUUID().toString() override fun id(): String { return randomID } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (intent != null && ACTION_STOP_VPN == intent.action) { (applicationContext as App).autoConnect = false close() return START_NOT_STICKY } val app = applicationContext as App if (intent != null && "android.net.VpnService" == intent.action) { // Start VPN and connect to it due to Always-on VPN val i = Intent(IPNReceiver.INTENT_CONNECT_VPN) i.setPackage(packageName) i.setClass(applicationContext, IPNReceiver::class.java) sendBroadcast(i) Libtailscale.requestVPN(this) app.setWantRunning(true) return START_STICKY } Libtailscale.requestVPN(this) if (app.vpnReady && app.autoConnect) { app.setWantRunning(true) } return START_STICKY } override public fun close() { stopForeground(true) Libtailscale.serviceDisconnect(this) val app = applicationContext as App app.setWantRunning(false) } override fun onDestroy() { close() super.onDestroy() } override fun onRevoke() { close() super.onRevoke() } private fun configIntent(): PendingIntent { return PendingIntent.getActivity( this, 0, Intent(this, MainActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) } private fun disallowApp(b: Builder, name: String) { try { b.addDisallowedApplication(name) } catch (e: PackageManager.NameNotFoundException) {} } 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. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) b.setUnderlyingNetworks(null) // Use all available networks. // RCS/Jibe https://github.com/tailscale/tailscale/issues/2322 disallowApp(b, "com.google.android.apps.messaging") // Stadia https://github.com/tailscale/tailscale/issues/3460 disallowApp(b, "com.google.stadia.android") // Android Auto https://github.com/tailscale/tailscale/issues/3828 disallowApp(b, "com.google.android.projection.gearhead") // GoPro https://github.com/tailscale/tailscale/issues/2554 disallowApp(b, "com.gopro.smarty") // Sonos https://github.com/tailscale/tailscale/issues/2548 disallowApp(b, "com.sonos.acr") disallowApp(b, "com.sonos.acr2") // Google Chromecast https://github.com/tailscale/tailscale/issues/3636 disallowApp(b, "com.google.android.apps.chromecast.app") return VPNServiceBuilder(b) } fun notify(title: String?, message: String?) { val builder: NotificationCompat.Builder = NotificationCompat.Builder(this, App.NOTIFY_CHANNEL_ID) .setSmallIcon(R.drawable.ic_notification) .setContentTitle(title) .setContentText(message) .setContentIntent(configIntent()) .setAutoCancel(true) .setOnlyAlertOnce(true) .setPriority(NotificationCompat.PRIORITY_DEFAULT) val nm: NotificationManagerCompat = NotificationManagerCompat.from(this) nm.notify(App.NOTIFY_NOTIFICATION_ID, builder.build()) } fun updateStatusNotification(title: String?, message: String?) { val builder: NotificationCompat.Builder = NotificationCompat.Builder(this, App.STATUS_CHANNEL_ID) .setSmallIcon(R.drawable.ic_notification) .setContentTitle(title) .setContentText(message) .setContentIntent(configIntent()) .setPriority(NotificationCompat.PRIORITY_LOW) startForeground(App.STATUS_NOTIFICATION_ID, builder.build()) } companion object { const val ACTION_REQUEST_VPN = "com.tailscale.ipn.REQUEST_VPN" const val ACTION_STOP_VPN = "com.tailscale.ipn.STOP_VPN" } }