android: prepare and connect VPN
-Create scope for IPNService, tied to the lifecycle of the VPN service -Create new IPNServiceManager, for actions taken by the backend -Call local.Start in the backend. This was missing before Updates tailscale/corp#18202 Signed-off-by: kari-ts <kari@tailscale.com>pull/208/head
parent
8b067ea102
commit
c92d347ac9
@ -1,143 +0,0 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
package com.tailscale.ipn;
|
|
||||||
|
|
||||||
import android.util.Log;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.net.VpnService;
|
|
||||||
import android.system.OsConstants;
|
|
||||||
import androidx.work.WorkManager;
|
|
||||||
import androidx.work.OneTimeWorkRequest;
|
|
||||||
import androidx.core.app.NotificationCompat;
|
|
||||||
import androidx.core.app.NotificationManagerCompat;
|
|
||||||
import com.tailscale.ipn.ui.service.IpnManager;
|
|
||||||
|
|
||||||
public class IPNService extends VpnService {
|
|
||||||
public static final String ACTION_REQUEST_VPN = "com.tailscale.ipn.REQUEST_VPN";
|
|
||||||
public static final String ACTION_STOP_VPN = "com.tailscale.ipn.STOP_VPN";
|
|
||||||
private IpnManager ipnManager;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(){
|
|
||||||
super.onCreate();
|
|
||||||
ipnManager = new IpnManager();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public int onStartCommand(Intent intent, int flags, int startId) {
|
|
||||||
if (intent != null && ACTION_STOP_VPN.equals(intent.getAction())) {
|
|
||||||
((App)getApplicationContext()).autoConnect = false;
|
|
||||||
close();
|
|
||||||
return START_NOT_STICKY;
|
|
||||||
}
|
|
||||||
if (intent != null && "android.net.VpnService".equals(intent.getAction())) {
|
|
||||||
// Start VPN and connect to it due to Always-on VPN
|
|
||||||
Intent i = new Intent(IPNReceiver.INTENT_CONNECT_VPN);
|
|
||||||
i.setPackage(getPackageName());
|
|
||||||
i.setClass(getApplicationContext(), com.tailscale.ipn.IPNReceiver.class);
|
|
||||||
sendBroadcast(i);
|
|
||||||
requestVPN();
|
|
||||||
connect();
|
|
||||||
return START_STICKY;
|
|
||||||
}
|
|
||||||
requestVPN();
|
|
||||||
App app = ((App)getApplicationContext());
|
|
||||||
if (app.vpnReady && app.autoConnect) {
|
|
||||||
ipnManager.connect();
|
|
||||||
}
|
|
||||||
return START_STICKY;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void close() {
|
|
||||||
stopForeground(true);
|
|
||||||
disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void onDestroy() {
|
|
||||||
close();
|
|
||||||
super.onDestroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void onRevoke() {
|
|
||||||
close();
|
|
||||||
super.onRevoke();
|
|
||||||
}
|
|
||||||
|
|
||||||
private PendingIntent configIntent() {
|
|
||||||
return PendingIntent.getActivity(this, 0, new Intent(this, IPNActivity.class),
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void disallowApp(VpnService.Builder b, String name) {
|
|
||||||
try {
|
|
||||||
b.addDisallowedApplication(name);
|
|
||||||
} catch (PackageManager.NameNotFoundException e) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected VpnService.Builder newBuilder() {
|
|
||||||
VpnService.Builder b = new VpnService.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
|
|
||||||
this.disallowApp(b, "com.google.android.apps.messaging");
|
|
||||||
|
|
||||||
// Stadia https://github.com/tailscale/tailscale/issues/3460
|
|
||||||
this.disallowApp(b, "com.google.stadia.android");
|
|
||||||
|
|
||||||
// Android Auto https://github.com/tailscale/tailscale/issues/3828
|
|
||||||
this.disallowApp(b, "com.google.android.projection.gearhead");
|
|
||||||
|
|
||||||
// GoPro https://github.com/tailscale/tailscale/issues/2554
|
|
||||||
this.disallowApp(b, "com.gopro.smarty");
|
|
||||||
|
|
||||||
// Sonos https://github.com/tailscale/tailscale/issues/2548
|
|
||||||
this.disallowApp(b, "com.sonos.acr");
|
|
||||||
this.disallowApp(b, "com.sonos.acr2");
|
|
||||||
|
|
||||||
// Google Chromecast https://github.com/tailscale/tailscale/issues/3636
|
|
||||||
this.disallowApp(b, "com.google.android.apps.chromecast.app");
|
|
||||||
|
|
||||||
return b;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void notify(String title, String message) {
|
|
||||||
NotificationCompat.Builder builder = new 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);
|
|
||||||
|
|
||||||
NotificationManagerCompat nm = NotificationManagerCompat.from(this);
|
|
||||||
nm.notify(App.NOTIFY_NOTIFICATION_ID, builder.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateStatusNotification(String title, String message) {
|
|
||||||
NotificationCompat.Builder builder = new 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());
|
|
||||||
}
|
|
||||||
|
|
||||||
private native void requestVPN();
|
|
||||||
|
|
||||||
private native void disconnect();
|
|
||||||
private native void connect();
|
|
||||||
}
|
|
@ -0,0 +1,145 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
package com.tailscale.ipn
|
||||||
|
|
||||||
|
import android.net.VpnService
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import android.util.Log
|
||||||
|
import android.os.Build
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.system.OsConstants
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import androidx.work.OneTimeWorkRequest
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.tailscale.ipn.ui.service.IpnServiceManager
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
||||||
|
class IPNService : VpnService() {
|
||||||
|
private val vpnServiceJob = Job()
|
||||||
|
private val vpnServiceScope = CoroutineScope(Dispatchers.Default + vpnServiceJob)
|
||||||
|
|
||||||
|
private var ipnServiceManager: IpnServiceManager? = null
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
ipnServiceManager = IpnServiceManager(vpnServiceScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
if (intent != null && ACTION_STOP_VPN == intent.getAction()) {
|
||||||
|
(getApplicationContext() as App).autoConnect = false
|
||||||
|
close()
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
if (intent != null && "android.net.VpnService" == intent.getAction()) {
|
||||||
|
// Start VPN and connect to it due to Always-on VPN
|
||||||
|
val i = Intent(IPNReceiver.INTENT_CONNECT_VPN)
|
||||||
|
i.setPackage(getPackageName())
|
||||||
|
i.setClass(getApplicationContext(), IPNReceiver::class.java)
|
||||||
|
sendBroadcast(i)
|
||||||
|
requestVPN()
|
||||||
|
ipnServiceManager?.connect()
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
requestVPN()
|
||||||
|
val app = applicationContext as App
|
||||||
|
if (app.vpnReady && app.autoConnect) {
|
||||||
|
ipnServiceManager?.connect()
|
||||||
|
}
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun close() {
|
||||||
|
stopForeground(true)
|
||||||
|
disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
close()
|
||||||
|
vpnServiceJob.cancel()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRevoke() {
|
||||||
|
close()
|
||||||
|
super.onRevoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun configIntent(): PendingIntent {
|
||||||
|
return PendingIntent.getActivity(this, 0, Intent(this, IPNActivity::class.java),
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun disallowApp(b: Builder, name: String) {
|
||||||
|
try {
|
||||||
|
b.addDisallowedApplication(name)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun newBuilder(): Builder {
|
||||||
|
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 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())
|
||||||
|
}
|
||||||
|
|
||||||
|
private external fun requestVPN()
|
||||||
|
private external fun disconnect()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val ACTION_REQUEST_VPN = "com.tailscale.ipn.REQUEST_VPN"
|
||||||
|
const val ACTION_STOP_VPN = "com.tailscale.ipn.STOP_VPN"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package com.tailscale.ipn.ui.service
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.util.Log
|
||||||
|
import com.tailscale.ipn.App
|
||||||
|
import com.tailscale.ipn.IPNReceiver
|
||||||
|
import com.tailscale.ipn.mdm.MDMSettings
|
||||||
|
import com.tailscale.ipn.ui.localapi.LocalApiClient
|
||||||
|
import com.tailscale.ipn.ui.model.Ipn
|
||||||
|
import com.tailscale.ipn.ui.notifier.Notifier
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
||||||
|
// Actions that can be taken by the backend
|
||||||
|
interface IpnServiceActions {
|
||||||
|
fun connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
class IpnServiceManager(scope: CoroutineScope) : IpnServiceActions {
|
||||||
|
private var notifier = Notifier()
|
||||||
|
|
||||||
|
var apiClient = LocalApiClient(scope)
|
||||||
|
var mdmSettings = MDMSettings()
|
||||||
|
val model = IpnModel(notifier, apiClient, scope)
|
||||||
|
|
||||||
|
override fun connect() {
|
||||||
|
val context = App.getApplication().applicationContext
|
||||||
|
val callback: (com.tailscale.ipn.ui.localapi.Result<Ipn.Prefs>) -> Unit = { result ->
|
||||||
|
if (result.successful) {
|
||||||
|
val prefs = result.success
|
||||||
|
Log.d("IpnManager","Connect: preferences updated successfully: $prefs")
|
||||||
|
} else if (result.failed) {
|
||||||
|
val error = result.error
|
||||||
|
Log.d("IpnManager","Connect: failed to update preferences: ${error?.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
model.setWantRunning(true, callback)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue