mdm: add Android syspolicy handler

Updates tailscale/corp#18202

Adds a syspolicy handler for Android in cmd/tailscale. This allows the Go code to use the syspolicy package to read values set by a system administrator using the Android RestrictionsManager.

Out of the box, this adds supports for a number of MDM policies that are fully integrated on the Go side, such as `ExitNodeID` (forced exit node functionality).

Signed-off-by: Andrea Gottardo <andrea@gottardo.me>
pull/195/head
Andrea Gottardo 2 months ago
parent c9b403a7ad
commit e8dfa1c833

@ -18,6 +18,7 @@ import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.RestrictionsManager;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.PackageInfo;
@ -57,6 +58,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
@ -67,6 +69,12 @@ import androidx.security.crypto.EncryptedSharedPreferences;
import androidx.security.crypto.MasterKey;
import androidx.browser.customtabs.CustomTabsIntent;
import com.tailscale.ipn.mdm.AlwaysNeverUserDecidesSetting;
import com.tailscale.ipn.mdm.BooleanSetting;
import com.tailscale.ipn.mdm.MDMSettings;
import com.tailscale.ipn.mdm.ShowHideSetting;
import com.tailscale.ipn.mdm.StringSetting;
import com.tailscale.ipn.ui.service.IpnManager;
import org.gioui.Gio;
@ -105,15 +113,15 @@ public class App extends Application {
createNotificationChannel(NOTIFY_CHANNEL_ID, "Notifications", NotificationManagerCompat.IMPORTANCE_DEFAULT);
createNotificationChannel(STATUS_CHANNEL_ID, "VPN Status", NotificationManagerCompat.IMPORTANCE_LOW);
createNotificationChannel(FILE_CHANNEL_ID, "File transfers", NotificationManagerCompat.IMPORTANCE_DEFAULT);
createNotificationChannel(FILE_CHANNEL_ID, "File transfers", NotificationManagerCompat.IMPORTANCE_DEFAULT);
_application = this;
}
// requestNetwork attempts to find the best network that matches the passed NetworkRequest. It is possible that
// this might return an unusuable network, eg a captive portal.
private void setAndRegisterNetworkCallbacks() {
connectivityManager.requestNetwork(dns.getDNSConfigNetworkRequest(), new ConnectivityManager.NetworkCallback(){
connectivityManager.requestNetwork(dns.getDNSConfigNetworkRequest(), new ConnectivityManager.NetworkCallback(){
@Override
public void onAvailable(Network network){
super.onAvailable(network);
@ -423,4 +431,42 @@ public class App extends Application {
UiModeManager mm = (UiModeManager)getSystemService(UI_MODE_SERVICE);
return mm.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;
}
/*
The following methods are called by the syspolicy handler from Go via JNI.
*/
boolean getSyspolicyBooleanValue(String key) {
RestrictionsManager manager = (RestrictionsManager) this.getSystemService(Context.RESTRICTIONS_SERVICE);
MDMSettings mdmSettings = new MDMSettings(manager);
BooleanSetting setting = BooleanSetting.valueOf(key);
return mdmSettings.get(setting);
}
String getSyspolicyStringValue(String key) {
RestrictionsManager manager = (RestrictionsManager) this.getSystemService(Context.RESTRICTIONS_SERVICE);
MDMSettings mdmSettings = new MDMSettings(manager);
// Before looking for a StringSetting matching the given key, Go could also be
// asking us for either a AlwaysNeverUserDecidesSetting or a ShowHideSetting.
// Check the enum cases for these two before looking for a StringSetting.
try {
AlwaysNeverUserDecidesSetting anuSetting = AlwaysNeverUserDecidesSetting.valueOf(key);
return mdmSettings.get(anuSetting).getValue();
} catch (IllegalArgumentException eanu) { // AlwaysNeverUserDecidesSetting does not exist
try {
ShowHideSetting showHideSetting = ShowHideSetting.valueOf(key);
return mdmSettings.get(showHideSetting).getValue();
} catch (IllegalArgumentException esh) {
try {
StringSetting stringSetting = StringSetting.valueOf(key);
String value = mdmSettings.get(stringSetting);
return Objects.requireNonNullElse(value, "");
} catch (IllegalArgumentException estr) {
android.util.Log.d("MDM", key+" is not defined on Android. Returning empty.");
return "";
}
}
}
}
}

@ -50,6 +50,7 @@ import (
"tailscale.com/tailcfg"
"tailscale.com/types/logid"
"tailscale.com/types/netmap"
"tailscale.com/util/syspolicy"
"tailscale.com/wgengine/router"
)
@ -281,6 +282,7 @@ func main() {
a.store = newStateStore(a.jvm, a.appCtx)
interfaces.RegisterInterfaceGetter(a.getInterfaces)
syspolicy.RegisterHandler(androidHandler{a: a})
go func() {
ctx := context.Background()
if err := a.runBackend(ctx); err != nil {

@ -0,0 +1,66 @@
// Copyright (c) 2024 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 main
import (
"log"
"github.com/tailscale/tailscale-android/cmd/jni"
"tailscale.com/util/syspolicy"
)
// androidHandler is a syspolicy handler for the Android version of the Tailscale client,
// which lets the main networking code read values set via the Android RestrictionsManager.
type androidHandler struct {
a *App
}
func (h androidHandler) ReadString(key string) (string, error) {
if key == "" {
return "", syspolicy.ErrNoSuchKey
}
retVal := ""
err := jni.Do(h.a.jvm, func(env *jni.Env) error {
cls := jni.GetObjectClass(env, h.a.appCtx)
m := jni.GetMethodID(env, cls, "getSyspolicyStringValue", "(Ljava/lang/String;)Ljava/lang/String;")
strObj, err := jni.CallObjectMethod(env, h.a.appCtx, m, jni.Value(jni.JavaString(env, key)))
if err != nil {
return err
}
retVal = jni.GoString(env, jni.String(strObj))
return nil
})
if err != nil {
log.Printf("syspolicy: failed to get string value via JNI: %v", err)
}
return retVal, err
}
func (h androidHandler) ReadBoolean(key string) (bool, error) {
if key == "" {
return false, syspolicy.ErrNoSuchKey
}
retVal := false
err := jni.Do(h.a.jvm, func(env *jni.Env) error {
cls := jni.GetObjectClass(env, h.a.appCtx)
m := jni.GetMethodID(env, cls, "getSyspolicyBooleanValue", "(Ljava/lang/String;)Z")
b, err := jni.CallBooleanMethod(env, h.a.appCtx, m, jni.Value(jni.JavaString(env, key)))
retVal = b
return err
})
if err != nil {
log.Printf("syspolicy: failed to get bool value via JNI: %v", err)
}
return retVal, err
}
func (h androidHandler) ReadUInt64(key string) (uint64, error) {
if key == "" {
return 0, syspolicy.ErrNoSuchKey
}
// TODO(angott): drop ReadUInt64 everywhere. We are not using it.
log.Fatalf("ReadUInt64 is not implemented on Android")
return 0, nil
}
Loading…
Cancel
Save