diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt index ae6ec07..129f3fd 100644 --- a/android/src/main/java/com/tailscale/ipn/App.kt +++ b/android/src/main/java/com/tailscale/ipn/App.kt @@ -15,6 +15,7 @@ import android.content.ContentResolver import android.content.ContentValues import android.content.Context import android.content.Intent +import android.content.RestrictionsManager import android.content.SharedPreferences import android.content.pm.PackageInfo import android.content.pm.PackageManager @@ -37,6 +38,10 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey +import com.tailscale.ipn.mdm.AlwaysNeverUserDecidesMDMSetting +import com.tailscale.ipn.mdm.BooleanMDMSetting +import com.tailscale.ipn.mdm.ShowHideMDMSetting +import com.tailscale.ipn.mdm.StringMDMSetting import com.tailscale.ipn.ui.localapi.Client import com.tailscale.ipn.ui.localapi.Request import com.tailscale.ipn.ui.model.Ipn @@ -451,4 +456,34 @@ class App : Application(), libtailscale.AppContext { return downloads } + + @Throws(IOException::class, GeneralSecurityException::class) + override fun getSyspolicyBooleanValue(key: String): Boolean { + val manager = getSystemService(Context.RESTRICTIONS_SERVICE) as RestrictionsManager + return BooleanMDMSetting(key, key).getFrom(manager.applicationRestrictions, this) + } + + @Throws(IOException::class, GeneralSecurityException::class) + override fun getSyspolicyStringValue(key: String): String { + val manager = getSystemService(Context.RESTRICTIONS_SERVICE) as RestrictionsManager + + // Create a single instance for each setting type with given key + val alwaysNeverSetting = AlwaysNeverUserDecidesMDMSetting(key, key) + val showHideSetting = ShowHideMDMSetting(key, key) + val stringSetting = StringMDMSetting(key, key) + + // Use a when statement to replace multiple if-else for cleaner logic + return when { + alwaysNeverSetting.keyExists(key) -> + alwaysNeverSetting.getFrom(manager.applicationRestrictions, this).value + showHideSetting.keyExists(key) -> + showHideSetting.getFrom(manager.applicationRestrictions, this).value + stringSetting.keyExists(key) -> + stringSetting.getFrom(manager.applicationRestrictions, this) ?: "" + else -> { + Log.d("MDM", "$key is not defined on Android. Returning empty.") + "" + } + } + } } diff --git a/android/src/main/java/com/tailscale/ipn/mdm/MDMSettingsDefinitions.kt b/android/src/main/java/com/tailscale/ipn/mdm/MDMSettingsDefinitions.kt index 8602d59..c4bb250 100644 --- a/android/src/main/java/com/tailscale/ipn/mdm/MDMSettingsDefinitions.kt +++ b/android/src/main/java/com/tailscale/ipn/mdm/MDMSettingsDefinitions.kt @@ -10,8 +10,23 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow abstract class MDMSetting(defaultValue: T, val key: String, val localizedTitle: String) { + private val keys = mutableSetOf() + + init { + registerKey(key) + } val flow: StateFlow = MutableStateFlow(defaultValue) + private fun registerKey(key: String) { + if (!keyExists(key)) { + keys.add(key) + } +} + +fun keyExists(key: String): Boolean { + return keys.contains(key) +} + fun setFrom(bundle: Bundle?, app: App) { val v = getFrom(bundle, app) flow.set(v) diff --git a/libtailscale/interfaces.go b/libtailscale/interfaces.go index 2aa7bb3..cc17561 100644 --- a/libtailscale/interfaces.go +++ b/libtailscale/interfaces.go @@ -45,6 +45,12 @@ type AppContext interface { // GetPlatformDNSConfig gets a string representation of the current DNS // configuration. GetPlatformDNSConfig() string + + // GetSyspolicyStringValue returns the current string value for the given system policy. + GetSyspolicyStringValue(key string) (string, error) + + // GetSyspolicyBooleanValue returns whether the given system policy is enabled. + GetSyspolicyBooleanValue(key string) (bool, error) } // IPNService corresponds to our IPNService in Java. diff --git a/libtailscale/syspolicy_handler.go b/libtailscale/syspolicy_handler.go new file mode 100644 index 0000000..9a233d5 --- /dev/null +++ b/libtailscale/syspolicy_handler.go @@ -0,0 +1,47 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package libtailscale + +import ( + "log" + + "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 := h.a.appCtx.GetSyspolicyStringValue(key) + if err != nil { + log.Printf("syspolicy: failed to get string value via gomobile: %v", err) + } + return retVal, err +} + +func (h androidHandler) ReadBoolean(key string) (bool, error) { + if key == "" { + return false, syspolicy.ErrNoSuchKey + } + retVal, err := h.a.appCtx.GetSyspolicyBooleanValue(key) + if err != nil { + log.Printf("syspolicy: failed to get bool value via gomobile: %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 +} diff --git a/libtailscale/tailscale.go b/libtailscale/tailscale.go index 93d7a58..8134dab 100644 --- a/libtailscale/tailscale.go +++ b/libtailscale/tailscale.go @@ -18,6 +18,7 @@ import ( "tailscale.com/types/logger" "tailscale.com/types/logid" "tailscale.com/util/clientmetric" + "tailscale.com/util/syspolicy" ) const defaultMTU = 1280 // minimalMTU from wgengine/userspace.go @@ -38,6 +39,7 @@ func newApp(dataDir, directFileRoot string, appCtx AppContext) Application { a.store = newStateStore(a.appCtx) interfaces.RegisterInterfaceGetter(a.getInterfaces) + syspolicy.RegisterHandler(androidHandler{a: a}) go func() { defer func() { if p := recover(); p != nil {