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>
kari/newapp
kari-ts 2 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() {
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);
}

@ -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();
}

@ -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})
}

@ -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

Loading…
Cancel
Save