From 9492b01946cd09859a169c90314bd12a113ca405 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Wed, 6 Mar 2024 09:36:44 -0800 Subject: [PATCH] cmd/tailscale, tailscale/ipn: fix alway-on VPN (#168) =If a ConnectEvent is received before the first notification, (as happens when a connection is attempted due to always-on after device reboot) create state.Prefs. -Create an intent to start the VPN worker in the case of an always-on intent received on device reboot -Rename onConnect channel to onVPNRequested, since this isn't doing the actual connecting Fixes tailscale/tailscale#2481 Signed-off-by: kari-ts --- .../src/main/java/com/tailscale/ipn/App.java | 4 +-- .../java/com/tailscale/ipn/IPNService.java | 28 +++++++++++++------ cmd/tailscale/callbacks.go | 14 +++++----- cmd/tailscale/main.go | 10 +++++-- 4 files changed, 37 insertions(+), 19 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/App.java b/android/src/main/java/com/tailscale/ipn/App.java index 0c6e9e1..c4a2529 100644 --- a/android/src/main/java/com/tailscale/ipn/App.java +++ b/android/src/main/java/com/tailscale/ipn/App.java @@ -138,13 +138,13 @@ public class App extends Application { public void startVPN() { Intent intent = new Intent(this, IPNService.class); - intent.setAction(IPNService.ACTION_CONNECT); + intent.setAction(IPNService.ACTION_REQUEST_VPN); startService(intent); } public void stopVPN() { Intent intent = new Intent(this, IPNService.class); - intent.setAction(IPNService.ACTION_DISCONNECT); + intent.setAction(IPNService.ACTION_STOP_VPN); startService(intent); } diff --git a/android/src/main/java/com/tailscale/ipn/IPNService.java b/android/src/main/java/com/tailscale/ipn/IPNService.java index fe23ae4..fee5b9e 100644 --- a/android/src/main/java/com/tailscale/ipn/IPNService.java +++ b/android/src/main/java/com/tailscale/ipn/IPNService.java @@ -10,6 +10,8 @@ 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 org.gioui.GioActivity; @@ -17,19 +19,29 @@ import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; public class IPNService extends VpnService { - public static final String ACTION_CONNECT = "com.tailscale.ipn.CONNECT"; - public static final String ACTION_DISCONNECT = "com.tailscale.ipn.DISCONNECT"; + public static final String ACTION_REQUEST_VPN = "com.tailscale.ipn.REQUEST_VPN"; + public static final String ACTION_STOP_VPN = "com.tailscale.ipn.STOP_VPN"; @Override public int onStartCommand(Intent intent, int flags, int startId) { - if (intent != null && ACTION_DISCONNECT.equals(intent.getAction())) { + 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; } - connect(); + requestVPN(); App app = ((App)getApplicationContext()); if (app.vpnReady && app.autoConnect) { - directConnect(); + connect(); } return START_STICKY; } @@ -119,8 +131,8 @@ public class IPNService extends VpnService { startForeground(App.STATUS_NOTIFICATION_ID, builder.build()); } - private native void connect(); - private native void disconnect(); + private native void requestVPN(); - public native void directConnect(); + private native void disconnect(); + private native void connect(); } diff --git a/cmd/tailscale/callbacks.go b/cmd/tailscale/callbacks.go index d18f260..be3c793 100644 --- a/cmd/tailscale/callbacks.go +++ b/cmd/tailscale/callbacks.go @@ -24,9 +24,9 @@ var ( // onVPNRevoked is notified whenever the VPN service is revoked. onVPNRevoked = make(chan struct{}, 1) - // onConnect receives global IPNService references when + // onVPNRequested receives global IPNService references when // a VPN connection is requested. - onConnect = make(chan jni.Object) + onVPNRequested = make(chan jni.Object) // onDisconnect receives global IPNService references when // disconnecting. onDisconnect = make(chan jni.Object) @@ -90,14 +90,14 @@ func notifyVPNClosed() { } } -//export Java_com_tailscale_ipn_IPNService_connect -func Java_com_tailscale_ipn_IPNService_connect(env *C.JNIEnv, this C.jobject) { +//export Java_com_tailscale_ipn_IPNService_requestVPN +func Java_com_tailscale_ipn_IPNService_requestVPN(env *C.JNIEnv, this C.jobject) { jenv := (*jni.Env)(unsafe.Pointer(env)) - onConnect <- jni.NewGlobalRef(jenv, jni.Object(this)) + onVPNRequested <- jni.NewGlobalRef(jenv, jni.Object(this)) } -//export Java_com_tailscale_ipn_IPNService_directConnect -func Java_com_tailscale_ipn_IPNService_directConnect(env *C.JNIEnv, this C.jobject) { +//export Java_com_tailscale_ipn_IPNService_connect +func Java_com_tailscale_ipn_IPNService_connect(env *C.JNIEnv, this C.jobject) { requestBackend(ConnectEvent{Enable: true}) } diff --git a/cmd/tailscale/main.go b/cmd/tailscale/main.go index 619e0bc..522bda9 100644 --- a/cmd/tailscale/main.go +++ b/cmd/tailscale/main.go @@ -493,6 +493,12 @@ func (a *App) runBackend(ctx context.Context) error { case LogoutEvent: go a.localAPI.Logout(ctx, a.backend) case ConnectEvent: + first := state.Prefs == nil + // A ConnectEvent might be sent before the first notification + // arrives, such as in the case of Always-on VPN. + if first { + state.Prefs = ipn.NewPrefs() + } state.Prefs.WantRunning = e.Enable go b.backend.SetPrefs(state.Prefs) case RouteAllEvent: @@ -504,7 +510,7 @@ func (a *App) runBackend(ctx context.Context) error { a.updateNotification(service, state.State, state.ExitStatus, state.Exit) } } - case s := <-onConnect: + case s := <-onVPNRequested: jni.Do(a.jvm, func(env *jni.Env) error { if jni.IsSameObject(env, s, service) { // We already have a reference. @@ -535,7 +541,7 @@ func (a *App) runBackend(ctx context.Context) error { return nil // even on error. see big TODO above. }) }) - log.Printf("onConnect: rebind required") + log.Printf("onVPNRequested: rebind required") // TODO(catzkorn): When we start the android application // we bind sockets before we have access to the VpnService.protect() // function which is needed to avoid routing loops. When we activate