Add intents (#87)

IPNReceiver: Add intents to connect and disconnect VPN

Added a new class IPNReceiver to listen to intents silently and connect and disconnect the VPN. This uses workers to avoid doing too much in the IPNReceiver which is to be avoided according to documentation.

Also includes a fix for vpn occasionally not starting. Think this was due to a race condition, but now only sets autoConnect to false when we know a connection is connecting or connected.

Fixes https://github.com/tailscale/tailscale/issues/3547
Updates https://github.com/tailscale/tailscale/issues/2481

Signed-off-by: Brett Jenkins <brett@brettjenkins.co.uk>
bradfitz/shared_user_dev
Brett Jenkins 1 year ago committed by GitHub
parent 24ba39121f
commit eb9599540c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -22,7 +22,7 @@ apply plugin: 'com.android.application'
android {
ndkVersion "23.1.7779620"
compileSdkVersion 30
compileSdkVersion 31
defaultConfig {
minSdkVersion 22
targetSdkVersion 31
@ -49,6 +49,7 @@ dependencies {
implementation "androidx.core:core:1.2.0"
implementation "androidx.browser:browser:1.2.0"
implementation "androidx.security:security-crypto:1.1.0-alpha03"
implementation "androidx.work:work-runtime:2.7.0"
implementation ':ipn@aar'
testCompile "junit:junit:4.12"

@ -55,6 +55,14 @@
<data android:mimeType="video/*" />
</intent-filter>
</activity>
<receiver android:name="IPNReceiver"
android:exported="true"
>
<intent-filter>
<action android:name="com.tailscale.ipn.CONNECT_VPN" />
<action android:name="com.tailscale.ipn.DISCONNECT_VPN" />
</intent-filter>
</receiver>
<service android:name=".IPNService"
android:permission="android.permission.BIND_VPN_SERVICE"
android:exported="false">

@ -106,6 +106,9 @@ public class App extends Application {
NetworkInfo active = cMgr.getActiveNetworkInfo();
// https://developer.android.com/training/monitoring-device-state/connectivity-status-type
boolean isConnected = active != null && active.isConnectedOrConnecting();
if (isConnected) {
((App)getApplicationContext()).autoConnect = false;
}
onConnectivityChanged(isConnected);
}
@ -161,11 +164,20 @@ public class App extends Application {
);
}
public boolean autoConnect = false;
public boolean vpnReady = false;
void setTileReady(boolean ready) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
return;
}
QuickToggleService.setReady(this, ready);
android.util.Log.d("App", "Set Tile Ready: " + ready + " " + autoConnect);
vpnReady = ready;
if (ready && autoConnect) {
startVPN();
}
}
void setTileStatus(boolean status) {
@ -333,7 +345,7 @@ public class App extends Application {
nm.notify(FILE_NOTIFICATION_ID, builder.build());
}
private void createNotificationChannel(String id, String name, int importance) {
public void createNotificationChannel(String id, String name, int importance) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
}

@ -0,0 +1,26 @@
// Copyright (c) 2023 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package com.tailscale.ipn;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import androidx.work.WorkManager;
import androidx.work.OneTimeWorkRequest;
public class IPNReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
WorkManager workManager = WorkManager.getInstance(context);
// On the relevant action, start the relevant worker, which can stay active for longer than this receiver can.
if (intent.getAction() == "com.tailscale.ipn.CONNECT_VPN") {
workManager.enqueue(new OneTimeWorkRequest.Builder(StartVPNWorker.class).build());
} else if (intent.getAction() == "com.tailscale.ipn.DISCONNECT_VPN") {
workManager.enqueue(new OneTimeWorkRequest.Builder(StopVPNWorker.class).build());
}
}
}

@ -26,6 +26,10 @@ public class IPNService extends VpnService {
return START_NOT_STICKY;
}
connect();
App app = ((App)getApplicationContext());
if (app.vpnReady && app.autoConnect) {
directConnect();
}
return START_STICKY;
}
@ -116,4 +120,6 @@ public class IPNService extends VpnService {
private native void connect();
private native void disconnect();
public native void directConnect();
}

@ -0,0 +1,65 @@
// Copyright (c) 2023 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package com.tailscale.ipn;
import androidx.work.Worker;
import android.content.Context;
import androidx.work.WorkerParameters;
import android.net.VpnService;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Build;
public final class StartVPNWorker extends Worker {
public StartVPNWorker(
Context appContext,
WorkerParameters workerParams) {
super(appContext, workerParams);
}
@Override public Result doWork() {
// We will start the VPN from the background
App app = ((App)getApplicationContext());
app.autoConnect = true;
// We need to make sure we prepare the VPN Service, just in case it isn't prepared.
Intent intent = VpnService.prepare(app);
if (intent == null) {
// If null then the VPN is already prepared and/or it's just been prepared because we have permission
app.startVPN();
return Result.success();
} else {
// This VPN possibly doesn't have permission, we need to display a notification which when clicked launches the intent provided.
android.util.Log.e("StartVPNWorker", "Tailscale doesn't have permission from the system to start VPN. Launching the intent provided.");
// Send notification
NotificationManager notificationManager = (NotificationManager) app.getSystemService(Context.NOTIFICATION_SERVICE);
String channelId = "start_vpn_channel";
// Use createNotificationChannel method from App.java
app.createNotificationChannel(channelId, "Start VPN Channel", NotificationManager.IMPORTANCE_DEFAULT);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
int pendingIntentFlags = PendingIntent.FLAG_ONE_SHOT | (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ? PendingIntent.FLAG_IMMUTABLE : 0);
PendingIntent pendingIntent = PendingIntent.getActivity(app, 0, intent, pendingIntentFlags);
Notification notification = new Notification.Builder(app, channelId)
.setContentTitle("Tailscale Connection Failed")
.setContentText("Tap here to renew permission.")
.setSmallIcon(R.drawable.ic_notification)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.build();
notificationManager.notify(1, notification);
return Result.failure();
}
}
}

@ -0,0 +1,25 @@
// Copyright (c) 2023 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package com.tailscale.ipn;
import androidx.work.Worker;
import android.content.Context;
import androidx.work.WorkerParameters;
public final class StopVPNWorker extends Worker {
public StopVPNWorker(
Context appContext,
WorkerParameters workerParams) {
super(appContext, workerParams);
}
@Override public Result doWork() {
disconnect();
return Result.success();
}
private native void disconnect();
}

@ -96,12 +96,27 @@ func Java_com_tailscale_ipn_IPNService_connect(env *C.JNIEnv, this C.jobject) {
onConnect <- 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) {
requestBackend(ConnectEvent{Enable: true})
}
//export Java_com_tailscale_ipn_IPNService_disconnect
func Java_com_tailscale_ipn_IPNService_disconnect(env *C.JNIEnv, this C.jobject) {
jenv := (*jni.Env)(unsafe.Pointer(env))
onDisconnect <- jni.NewGlobalRef(jenv, jni.Object(this))
}
//export Java_com_tailscale_ipn_StartVPNWorker_connect
func Java_com_tailscale_ipn_StartVPNWorker_connect(env *C.JNIEnv, this C.jobject) {
requestBackend(ConnectEvent{Enable: true})
}
//export Java_com_tailscale_ipn_StopVPNWorker_disconnect
func Java_com_tailscale_ipn_StopVPNWorker_disconnect(env *C.JNIEnv, this C.jobject) {
requestBackend(ConnectEvent{Enable: false})
}
//export Java_com_tailscale_ipn_App_onConnectivityChanged
func Java_com_tailscale_ipn_App_onConnectivityChanged(env *C.JNIEnv, cls C.jclass, connected C.jboolean) {
select {

Loading…
Cancel
Save