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 <kari@tailscale.com>
pull/180/head
kari-ts 7 months ago committed by GitHub
parent bb7ea7cf9f
commit 9492b01946
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -138,13 +138,13 @@ public class App extends Application {
public void startVPN() { public void startVPN() {
Intent intent = new Intent(this, IPNService.class); Intent intent = new Intent(this, IPNService.class);
intent.setAction(IPNService.ACTION_CONNECT); intent.setAction(IPNService.ACTION_REQUEST_VPN);
startService(intent); startService(intent);
} }
public void stopVPN() { public void stopVPN() {
Intent intent = new Intent(this, IPNService.class); Intent intent = new Intent(this, IPNService.class);
intent.setAction(IPNService.ACTION_DISCONNECT); intent.setAction(IPNService.ACTION_STOP_VPN);
startService(intent); startService(intent);
} }

@ -10,6 +10,8 @@ import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.VpnService; import android.net.VpnService;
import android.system.OsConstants; import android.system.OsConstants;
import androidx.work.WorkManager;
import androidx.work.OneTimeWorkRequest;
import org.gioui.GioActivity; import org.gioui.GioActivity;
@ -17,19 +19,29 @@ import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat; import androidx.core.app.NotificationManagerCompat;
public class IPNService extends VpnService { public class IPNService extends VpnService {
public static final String ACTION_CONNECT = "com.tailscale.ipn.CONNECT"; public static final String ACTION_REQUEST_VPN = "com.tailscale.ipn.REQUEST_VPN";
public static final String ACTION_DISCONNECT = "com.tailscale.ipn.DISCONNECT"; public static final String ACTION_STOP_VPN = "com.tailscale.ipn.STOP_VPN";
@Override public int onStartCommand(Intent intent, int flags, int startId) { @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; ((App)getApplicationContext()).autoConnect = false;
close(); close();
return START_NOT_STICKY; return START_NOT_STICKY;
} }
connect(); 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()); App app = ((App)getApplicationContext());
if (app.vpnReady && app.autoConnect) { if (app.vpnReady && app.autoConnect) {
directConnect(); connect();
} }
return START_STICKY; return START_STICKY;
} }
@ -119,8 +131,8 @@ public class IPNService extends VpnService {
startForeground(App.STATUS_NOTIFICATION_ID, builder.build()); startForeground(App.STATUS_NOTIFICATION_ID, builder.build());
} }
private native void connect(); private native void requestVPN();
private native void disconnect();
public native void directConnect(); private native void disconnect();
private native void connect();
} }

@ -24,9 +24,9 @@ var (
// onVPNRevoked is notified whenever the VPN service is revoked. // onVPNRevoked is notified whenever the VPN service is revoked.
onVPNRevoked = make(chan struct{}, 1) onVPNRevoked = make(chan struct{}, 1)
// onConnect receives global IPNService references when // onVPNRequested receives global IPNService references when
// a VPN connection is requested. // a VPN connection is requested.
onConnect = make(chan jni.Object) onVPNRequested = make(chan jni.Object)
// onDisconnect receives global IPNService references when // onDisconnect receives global IPNService references when
// disconnecting. // disconnecting.
onDisconnect = make(chan jni.Object) onDisconnect = make(chan jni.Object)
@ -90,14 +90,14 @@ func notifyVPNClosed() {
} }
} }
//export Java_com_tailscale_ipn_IPNService_connect //export Java_com_tailscale_ipn_IPNService_requestVPN
func Java_com_tailscale_ipn_IPNService_connect(env *C.JNIEnv, this C.jobject) { func Java_com_tailscale_ipn_IPNService_requestVPN(env *C.JNIEnv, this C.jobject) {
jenv := (*jni.Env)(unsafe.Pointer(env)) 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 //export Java_com_tailscale_ipn_IPNService_connect
func Java_com_tailscale_ipn_IPNService_directConnect(env *C.JNIEnv, this C.jobject) { func Java_com_tailscale_ipn_IPNService_connect(env *C.JNIEnv, this C.jobject) {
requestBackend(ConnectEvent{Enable: true}) requestBackend(ConnectEvent{Enable: true})
} }

@ -493,6 +493,12 @@ func (a *App) runBackend(ctx context.Context) error {
case LogoutEvent: case LogoutEvent:
go a.localAPI.Logout(ctx, a.backend) go a.localAPI.Logout(ctx, a.backend)
case ConnectEvent: 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 state.Prefs.WantRunning = e.Enable
go b.backend.SetPrefs(state.Prefs) go b.backend.SetPrefs(state.Prefs)
case RouteAllEvent: case RouteAllEvent:
@ -504,7 +510,7 @@ func (a *App) runBackend(ctx context.Context) error {
a.updateNotification(service, state.State, state.ExitStatus, state.Exit) a.updateNotification(service, state.State, state.ExitStatus, state.Exit)
} }
} }
case s := <-onConnect: case s := <-onVPNRequested:
jni.Do(a.jvm, func(env *jni.Env) error { jni.Do(a.jvm, func(env *jni.Env) error {
if jni.IsSameObject(env, s, service) { if jni.IsSameObject(env, s, service) {
// We already have a reference. // 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. 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 // TODO(catzkorn): When we start the android application
// we bind sockets before we have access to the VpnService.protect() // we bind sockets before we have access to the VpnService.protect()
// function which is needed to avoid routing loops. When we activate // function which is needed to avoid routing loops. When we activate

Loading…
Cancel
Save