From d6102c75de1533b5dd0574f64f08d0637df84b05 Mon Sep 17 00:00:00 2001 From: Michael Nahkies Date: Sun, 31 Aug 2025 11:47:39 +0100 Subject: [PATCH 1/3] feat: support disabling remote log uploads Updates https://github.com/tailscale/tailscale/issues/13174 - adds a new switch to the settings page for enabling/disabling remote log uploads - calls the `Disable` function from the `logtail` package during init when the setting is turn off ref: https://pkg.go.dev/tailscale.com/logtail#Disable Signed-off-by: Michael Nahkies --- .../src/main/java/com/tailscale/ipn/App.kt | 13 ++++++++++++ .../com/tailscale/ipn/ui/view/SettingsView.kt | 21 +++++++++++++++++++ .../ipn/ui/viewModel/SettingsViewModel.kt | 9 ++++++++ android/src/main/res/values/strings.xml | 2 ++ libtailscale/backend.go | 2 +- libtailscale/interfaces.go | 3 +++ libtailscale/tailscale.go | 15 ++++++++++++- 7 files changed, 63 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt index f89821e402..132901970b 100644 --- a/android/src/main/java/com/tailscale/ipn/App.kt +++ b/android/src/main/java/com/tailscale/ipn/App.kt @@ -292,6 +292,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 +379,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 +544,14 @@ open class UninitializedApp : Application() { return builder.build() } + fun getIsClientLoggingEnabled(): Boolean { + 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/ui/view/SettingsView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt index 2dc187fc26..cb57fa1373 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,16 @@ 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(R.string.client_remote_logging_enabled_subtitle), + isOn = isClientRemoteLoggingEnabled, + onToggle = { + viewModel.toggleIsClientRemoteLoggingEnabled() + }) + if (!AndroidTVUtil.isAndroidTV()) { Lists.ItemDivider() Setting.Text(R.string.permissions, onClick = settingsNav.onNavigateToPermissions) @@ -175,6 +186,7 @@ object Setting { fun Switch( titleRes: Int = 0, title: String? = null, + subtitle: String? = null, isOn: Boolean, enabled: Boolean = true, onToggle: (Boolean) -> Unit = {} @@ -187,6 +199,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..78d703b1a9 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -346,6 +346,8 @@ 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 + Equivalent to --no-logs-no-support on Linux.\nChanges require restarting the app to take effect. 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) From 0603fee2a31c57811afd73752db2c705718a0e1f Mon Sep 17 00:00:00 2001 From: Michael Nahkies Date: Mon, 1 Sep 2025 08:55:02 +0100 Subject: [PATCH 2/3] fix: tweak copy Signed-off-by: Michael Nahkies --- .../com/tailscale/ipn/ui/view/SettingsView.kt | 22 +++++++++---------- android/src/main/res/values/strings.xml | 2 +- 2 files changed, 11 insertions(+), 13 deletions(-) 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 cb57fa1373..75ccc4d414 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 @@ -110,12 +110,10 @@ fun SettingsView( Lists.ItemDivider() Setting.Switch( - R.string.client_remote_logging_enabled, - subtitle = stringResource(R.string.client_remote_logging_enabled_subtitle), - isOn = isClientRemoteLoggingEnabled, - onToggle = { - viewModel.toggleIsClientRemoteLoggingEnabled() - }) + R.string.client_remote_logging_enabled, + subtitle = stringResource(R.string.client_remote_logging_enabled_subtitle), + isOn = isClientRemoteLoggingEnabled, + onToggle = { viewModel.toggleIsClientRemoteLoggingEnabled() }) if (!AndroidTVUtil.isAndroidTV()) { Lists.ItemDivider() @@ -201,12 +199,12 @@ object Setting { }, supportingContent = subtitle?.let { - { - Text( - it, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant) - } + { + Text( + it, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant) + } }, trailingContent = { TintedSwitch(checked = isOn, onCheckedChange = onToggle, enabled = enabled) diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index 78d703b1a9..c681adebff 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -347,7 +347,7 @@ 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 - Equivalent to --no-logs-no-support on Linux.\nChanges require restarting the app to take effect. + Whether debug logs are uploaded to Tailscale support. When disabled no support or network flow logs.\nChanges require restarting the app to take effect. Specifies a device name to be used instead of the automatic default. Hostname Failed to save From 04fd66c55f7e29b10ebab1b64890adf7356cfb2e Mon Sep 17 00:00:00 2001 From: Michael Nahkies Date: Fri, 5 Sep 2025 09:01:37 +0100 Subject: [PATCH 3/3] fix: force client logging on when any mdm is configured Signed-off-by: Michael Nahkies --- android/src/main/java/com/tailscale/ipn/App.kt | 13 +++++++++++-- .../main/java/com/tailscale/ipn/mdm/MDMSettings.kt | 14 ++++++++++++-- .../ipn/mdm/MDMSettingsChangedReceiver.kt | 9 +++++++++ .../java/com/tailscale/ipn/ui/view/SettingsView.kt | 7 ++++++- android/src/main/res/values/strings.xml | 1 + 5 files changed, 39 insertions(+), 5 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt index 132901970b..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, @@ -545,6 +547,13 @@ open class UninitializedApp : Application() { } 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) } 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 75ccc4d414..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 @@ -111,8 +111,13 @@ fun SettingsView( Lists.ItemDivider() Setting.Switch( R.string.client_remote_logging_enabled, - subtitle = stringResource(R.string.client_remote_logging_enabled_subtitle), + 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()) { diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index c681adebff..4f94eefaca 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -348,6 +348,7 @@ 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