From e8dfa1c833a20aa639e44dea658e071507968ca1 Mon Sep 17 00:00:00 2001 From: Andrea Gottardo Date: Tue, 12 Mar 2024 14:49:59 -0700 Subject: [PATCH] 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 --- .../src/main/java/com/tailscale/ipn/App.java | 52 ++++++++++++++- cmd/tailscale/main.go | 2 + cmd/tailscale/syspolicy_handler.go | 66 +++++++++++++++++++ 3 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 cmd/tailscale/syspolicy_handler.go diff --git a/android/src/main/java/com/tailscale/ipn/App.java b/android/src/main/java/com/tailscale/ipn/App.java index ca3ad30..1dd21a9 100644 --- a/android/src/main/java/com/tailscale/ipn/App.java +++ b/android/src/main/java/com/tailscale/ipn/App.java @@ -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 ""; + } + } + } + } } diff --git a/cmd/tailscale/main.go b/cmd/tailscale/main.go index f477190..3ab8532 100644 --- a/cmd/tailscale/main.go +++ b/cmd/tailscale/main.go @@ -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 { diff --git a/cmd/tailscale/syspolicy_handler.go b/cmd/tailscale/syspolicy_handler.go new file mode 100644 index 0000000..724bf1d --- /dev/null +++ b/cmd/tailscale/syspolicy_handler.go @@ -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 +}