diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt index f89821e402..3326de33e7 100644 --- a/android/src/main/java/com/tailscale/ipn/App.kt +++ b/android/src/main/java/com/tailscale/ipn/App.kt @@ -146,6 +146,10 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { } private fun initializeApp() { + // Read MDM settings as early as possible, before starting the go backend. + val rm = getSystemService(Context.RESTRICTIONS_SERVICE) as RestrictionsManager + MDMSettings.update(this, rm, true) + // Check if a directory URI has already been stored. val storedUri = getStoredDirectoryUri() if (storedUri != null && storedUri.toString().startsWith("content://")) { @@ -158,8 +162,6 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { NetworkChangeCallback.monitorDnsChanges(connectivityManager, dns) initViewModels() applicationScope.launch { - val rm = getSystemService(Context.RESTRICTIONS_SERVICE) as RestrictionsManager - MDMSettings.update(get(), rm) Notifier.state.collect { _ -> combine(Notifier.state, MDMSettings.forceEnabled.flow, Notifier.prefs, Notifier.netmap) { state, @@ -292,6 +294,10 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { return packageManager.hasSystemFeature("android.hardware.type.pc") } + override fun isClientLoggingEnabled(): Boolean { + return getIsClientLoggingEnabled() + } + override fun getInterfacesAsString(): String { val interfaces: ArrayList = java.util.Collections.list(NetworkInterface.getNetworkInterfaces()) @@ -375,6 +381,7 @@ open class UninitializedApp : Application() { // the VPN (i.e. we're logged in and machine is authorized). private const val ABLE_TO_START_VPN_KEY = "ableToStartVPN" private const val DISALLOWED_APPS_KEY = "disallowedApps" + private const val IS_CLIENT_LOGGING_ENABLED_KEY = "isClientLoggingEnabled" // File for shared preferences that are not encrypted. private const val UNENCRYPTED_PREFERENCES = "unencrypted" private lateinit var appInstance: UninitializedApp @@ -539,6 +546,21 @@ open class UninitializedApp : Application() { return builder.build() } + fun getIsClientLoggingEnabled(): Boolean { + + // Force client logging to be enabled, when the device is managed by MDM + // Later this could become a dedicated MDMSetting / restriction. + if (MDMSettings.isMDMConfigured) { + return true + } + + return getUnencryptedPrefs().getBoolean(IS_CLIENT_LOGGING_ENABLED_KEY, true) + } + + fun updateIsClientLoggingEnabled(value: Boolean) { + getUnencryptedPrefs().edit().putBoolean(IS_CLIENT_LOGGING_ENABLED_KEY, value).apply() + } + fun updateUserDisallowedPackageNames(packageNames: List) { if (packageNames.any { it.isEmpty() }) { TSLog.e(TAG, "updateUserDisallowedPackageNames called with empty packageName(s)") diff --git a/android/src/main/java/com/tailscale/ipn/mdm/MDMSettings.kt b/android/src/main/java/com/tailscale/ipn/mdm/MDMSettings.kt index 34b341fc76..5aa5b048e4 100644 --- a/android/src/main/java/com/tailscale/ipn/mdm/MDMSettings.kt +++ b/android/src/main/java/com/tailscale/ipn/mdm/MDMSettings.kt @@ -18,6 +18,11 @@ object MDMSettings { // to the backend. class NoSuchKeyException : Exception("no such key") + // We default this to true, so that stricter behavior is used during initialization, + // prior to receiving MDM restrictions. + var isMDMConfigured = true + private set + val forceEnabled = BooleanMDMSetting("ForceEnabled", "Force Enabled Connection Toggle") // Handled on the backed @@ -117,10 +122,15 @@ object MDMSettings { val allSettingsByKey by lazy { allSettings.associateBy { it.key } } - fun update(app: App, restrictionsManager: RestrictionsManager?) { + fun update(app: App, restrictionsManager: RestrictionsManager?, skipNotify: Boolean = false) { val bundle = restrictionsManager?.applicationRestrictions val preferences = lazy { app.getEncryptedPrefs() } allSettings.forEach { it.setFrom(bundle, preferences) } - app.notifyPolicyChanged() + + isMDMConfigured = bundle?.isEmpty == true + + if (!skipNotify) { + app.notifyPolicyChanged() + } } } diff --git a/android/src/main/java/com/tailscale/ipn/mdm/MDMSettingsChangedReceiver.kt b/android/src/main/java/com/tailscale/ipn/mdm/MDMSettingsChangedReceiver.kt index d54129d831..76473082fb 100644 --- a/android/src/main/java/com/tailscale/ipn/mdm/MDMSettingsChangedReceiver.kt +++ b/android/src/main/java/com/tailscale/ipn/mdm/MDMSettingsChangedReceiver.kt @@ -16,7 +16,16 @@ class MDMSettingsChangedReceiver : BroadcastReceiver() { TSLog.d("syspolicy", "MDM settings changed") val restrictionsManager = context?.getSystemService(Context.RESTRICTIONS_SERVICE) as RestrictionsManager + + val previouslyIsMDMEnabled = MDMSettings.isMDMConfigured + MDMSettings.update(App.get(), restrictionsManager) + + if (MDMSettings.isMDMConfigured && !previouslyIsMDMEnabled) { + // async MDM settings updated from disabled -> enabled. restart to ensure + // correctly applied (particularly forcing client logs on). + // TODO: actually restart + } } } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt index 2dc187fc26..6e86e87f02 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt @@ -58,6 +58,7 @@ fun SettingsView( val isVPNPrepared by appViewModel.vpnPrepared.collectAsState() val showTailnetLock by MDMSettings.manageTailnetLock.flow.collectAsState() val useTailscaleSubnets by MDMSettings.useTailscaleSubnets.flow.collectAsState() + val isClientRemoteLoggingEnabled by viewModel.isClientRemoteLoggingEnabled.collectAsState() Scaffold( topBar = { @@ -106,6 +107,19 @@ fun SettingsView( Lists.ItemDivider() Setting.Text(R.string.subnet_routing, onClick = settingsNav.onNavigateToSubnetRouting) } + + Lists.ItemDivider() + Setting.Switch( + R.string.client_remote_logging_enabled, + subtitle = + stringResource( + if (MDMSettings.isMDMConfigured) + R.string.client_remote_logging_enabled_subtitle_mdm + else R.string.client_remote_logging_enabled_subtitle), + isOn = isClientRemoteLoggingEnabled, + enabled = !MDMSettings.isMDMConfigured, + onToggle = { viewModel.toggleIsClientRemoteLoggingEnabled() }) + if (!AndroidTVUtil.isAndroidTV()) { Lists.ItemDivider() Setting.Text(R.string.permissions, onClick = settingsNav.onNavigateToPermissions) @@ -175,6 +189,7 @@ object Setting { fun Switch( titleRes: Int = 0, title: String? = null, + subtitle: String? = null, isOn: Boolean, enabled: Boolean = true, onToggle: (Boolean) -> Unit = {} @@ -187,6 +202,15 @@ object Setting { style = MaterialTheme.typography.bodyMedium, ) }, + supportingContent = + subtitle?.let { + { + Text( + it, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant) + } + }, trailingContent = { TintedSwitch(checked = isOn, onCheckedChange = onToggle, enabled = enabled) }) diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/SettingsViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/SettingsViewModel.kt index b9343c96b5..e0fbc912fc 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/SettingsViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/SettingsViewModel.kt @@ -4,6 +4,7 @@ package com.tailscale.ipn.ui.viewModel import androidx.lifecycle.viewModelScope +import com.tailscale.ipn.App import com.tailscale.ipn.ui.localapi.Client import com.tailscale.ipn.ui.notifier.Notifier import com.tailscale.ipn.ui.util.LoadingIndicator @@ -34,8 +35,11 @@ class SettingsViewModel : IpnViewModel() { val tailNetLockEnabled: StateFlow = MutableStateFlow(null) // True if tailscaleDNS is enabled. nil if not yet known. val corpDNSEnabled: StateFlow = MutableStateFlow(null) + val isClientRemoteLoggingEnabled: StateFlow = MutableStateFlow(true) init { + isClientRemoteLoggingEnabled.set(App.get().isClientLoggingEnabled()) + viewModelScope.launch { Notifier.netmap.collect { netmap -> isAdmin.set(netmap?.SelfNode?.isAdmin ?: false) } } @@ -52,4 +56,9 @@ class SettingsViewModel : IpnViewModel() { } } } + + fun toggleIsClientRemoteLoggingEnabled() { + isClientRemoteLoggingEnabled.set(!isClientRemoteLoggingEnabled.value) + App.get().updateIsClientLoggingEnabled(isClientRemoteLoggingEnabled.value) + } } diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index 97d7edc514..4f94eefaca 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -346,6 +346,9 @@ Run as subnet router Route traffic according to your network\'s rules. Some networks require this to access IP addresses that don\'t start with 100.x.y.z. Subnet routing + Remote client logging + Whether debug logs are uploaded to Tailscale support. When disabled no support or network flow logs.\nChanges require restarting the app to take effect. + Client logging is always enabled for devices under remote management. Specifies a device name to be used instead of the automatic default. Hostname Failed to save diff --git a/libtailscale/backend.go b/libtailscale/backend.go index 8503b77c90..531275e9b9 100644 --- a/libtailscale/backend.go +++ b/libtailscale/backend.go @@ -288,7 +288,7 @@ func (a *App) newBackend(dataDir string, appCtx AppContext, store *stateStore, log.Printf("netmon.New: %w", err) } b.netMon = netMon - b.setupLogs(dataDir, logID, logf, sys.HealthTracker()) + b.setupLogs(dataDir, logID, logf, sys.HealthTracker(), a.isClientLoggingEnabled()) dialer := new(tsdial.Dialer) vf := &VPNFacade{ SetBoth: b.setCfg, diff --git a/libtailscale/interfaces.go b/libtailscale/interfaces.go index ca130706c8..db7d117f36 100644 --- a/libtailscale/interfaces.go +++ b/libtailscale/interfaces.go @@ -48,6 +48,9 @@ type AppContext interface { // IsChromeOS reports whether we're on a ChromeOS device. IsChromeOS() (bool, error) + // IsClientLoggingEnabled reports whether the user has enabled remote client logging. + IsClientLoggingEnabled() (bool, error) + // GetInterfacesAsString gets a string representation of all network // interfaces. GetInterfacesAsString() (string, error) diff --git a/libtailscale/tailscale.go b/libtailscale/tailscale.go index c03e6f5693..9d6491381b 100644 --- a/libtailscale/tailscale.go +++ b/libtailscale/tailscale.go @@ -95,8 +95,16 @@ func (a *App) isChromeOS() bool { return isChromeOS } +func (a *App) isClientLoggingEnabled() bool { + isClientLoggingEnabled, err := a.appCtx.IsClientLoggingEnabled() + if err != nil { + panic(err) + } + return isClientLoggingEnabled +} + // SetupLogs sets up remote logging. -func (b *backend) setupLogs(logDir string, logID logid.PrivateID, logf logger.Logf, health *health.Tracker) { +func (b *backend) setupLogs(logDir string, logID logid.PrivateID, logf logger.Logf, health *health.Tracker, enableUpload bool) { if b.netMon == nil { panic("netMon must be created prior to SetupLogs") } @@ -126,6 +134,11 @@ func (b *backend) setupLogs(logDir string, logID logid.PrivateID, logf logger.Lo b.logger = logtail.NewLogger(logcfg, logf) + if !enableUpload { + log.Printf("disabling remote log upload") + logtail.Disable() + } + log.SetFlags(0) log.SetOutput(b.logger)