Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions android/src/main/java/com/tailscale/ipn/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,10 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
return packageManager.hasSystemFeature("android.hardware.type.pc")
}

override fun isClientLoggingEnabled(): Boolean {
return getIsClientLoggingEnabled()
}

@Serializable
data class AddrJson(
val ip: String,
Expand Down Expand Up @@ -519,6 +523,7 @@ open class UninitializedApp : Application() {
private const val SELECTED_APPS_KEY = "disallowedApps"
private const val ALLOW_SELECTED_APPS_KEY = "allowSelectedApps"

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
Expand Down Expand Up @@ -700,6 +705,22 @@ 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()
App.get().getLibtailscaleApp().setClientLoggingEnabled(getIsClientLoggingEnabled())
}

fun updateUserSelectedPackages(packageNames: List<String>) {
if (packageNames.any { it.isEmpty() }) {
TSLog.e(TAG, "updateUserSelectedPackage called with empty packageName(s)")
Expand Down
6 changes: 6 additions & 0 deletions android/src/main/java/com/tailscale/ipn/mdm/MDMSettings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ object MDMSettings {
// MDM restriction keys
const val KEY_HARDWARE_ATTESTATION = "HardwareAttestation"

// 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
Expand Down Expand Up @@ -130,6 +135,7 @@ object MDMSettings {
fun loadFrom(preferences: Lazy<SharedPreferences>, restrictionsManager: RestrictionsManager?) {
val bundle = restrictionsManager?.applicationRestrictions
allSettings.forEach { it.setFrom(bundle, preferences) }
isMDMConfigured = bundle != null && !bundle.isEmpty
}

fun update(app: App, restrictionsManager: RestrictionsManager?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ class MDMSettingsChangedReceiver : BroadcastReceiver() {
TSLog.d("syspolicy", "MDM settings changed")
val restrictionsManager =
context?.getSystemService(Context.RESTRICTIONS_SERVICE) as RestrictionsManager

MDMSettings.update(App.get(), restrictionsManager)

// MDM state may have flipped the effective client-logging value
// (getIsClientLoggingEnabled forces true under MDM); push the
// current effective value so the backend toggles immediately.
App.get().getLibtailscaleApp().setClientLoggingEnabled(App.get().getIsClientLoggingEnabled())
}
}
}
59 changes: 59 additions & 0 deletions android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,18 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalUriHandler
Expand Down Expand Up @@ -58,6 +63,8 @@ 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()
var showDisableLoggingDialog by remember { mutableStateOf(false) }

Scaffold(
topBar = {
Expand Down Expand Up @@ -106,6 +113,25 @@ 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 = {
if (isClientRemoteLoggingEnabled) {
showDisableLoggingDialog = true
} else {
viewModel.toggleIsClientRemoteLoggingEnabled()
}
})

if (!AndroidTVUtil.isAndroidTV()) {
Lists.ItemDivider()
Setting.Text(R.string.permissions, onClick = settingsNav.onNavigateToPermissions)
Expand Down Expand Up @@ -135,6 +161,29 @@ fun SettingsView(
}
}
}

if (showDisableLoggingDialog) {
AlertDialog(
onDismissRequest = { showDisableLoggingDialog = false },
title = { Text(stringResource(R.string.client_remote_logging_disable_confirm_title)) },
text = { Text(stringResource(R.string.client_remote_logging_disable_confirm_message)) },
confirmButton = {
TextButton(
onClick = {
showDisableLoggingDialog = false
viewModel.toggleIsClientRemoteLoggingEnabled()
}) {
Text(
stringResource(R.string.client_remote_logging_disable_confirm_button),
color = MaterialTheme.colorScheme.error)
}
},
dismissButton = {
TextButton(onClick = { showDisableLoggingDialog = false }) {
Text(stringResource(R.string.cancel))
}
})
}
}

object Setting {
Expand Down Expand Up @@ -175,6 +224,7 @@ object Setting {
fun Switch(
titleRes: Int = 0,
title: String? = null,
subtitle: String? = null,
isOn: Boolean,
enabled: Boolean = true,
onToggle: (Boolean) -> Unit = {}
Expand All @@ -187,6 +237,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)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -34,8 +35,11 @@ class SettingsViewModel : IpnViewModel() {
val tailNetLockEnabled: StateFlow<Boolean?> = MutableStateFlow(null)
// True if tailscaleDNS is enabled. nil if not yet known.
val corpDNSEnabled: StateFlow<Boolean?> = MutableStateFlow(null)
val isClientRemoteLoggingEnabled: StateFlow<Boolean> = MutableStateFlow(true)

init {
isClientRemoteLoggingEnabled.set(App.get().isClientLoggingEnabled())

viewModelScope.launch {
Notifier.netmap.collect { netmap -> isAdmin.set(netmap?.SelfNode?.isAdmin ?: false) }
}
Expand All @@ -52,4 +56,9 @@ class SettingsViewModel : IpnViewModel() {
}
}
}

fun toggleIsClientRemoteLoggingEnabled() {
isClientRemoteLoggingEnabled.set(!isClientRemoteLoggingEnabled.value)
App.get().updateIsClientLoggingEnabled(isClientRemoteLoggingEnabled.value)
}
}
6 changes: 6 additions & 0 deletions android/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,12 @@
<string name="run_as_subnet_router">Run as subnet router</string>
<string name="use_tailscale_subnets_subtitle">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.</string>
<string name="subnet_routing">Subnet routing</string>
<string name="client_remote_logging_enabled">Remote client logging</string>
<string name="client_remote_logging_enabled_subtitle">Whether debug &amp; opt-in flow logs are uploaded.</string>
<string name="client_remote_logging_enabled_subtitle_mdm">Client logging is always enabled for devices under remote management.</string>
<string name="client_remote_logging_disable_confirm_title">Disable remote client logging?</string>
<string name="client_remote_logging_disable_confirm_message">Disabling remote client logging will break Network Flow Logs if enabled and required by your tailnet admin, and will prevent Tailscale Support from being able to help debug problems with this device.</string>
<string name="client_remote_logging_disable_confirm_button">Yes, disable</string>
<string name="specifies_a_device_name_to_be_used_instead_of_the_automatic_default">Specifies a device name to be used instead of the automatic default.</string>
<string name="hostname">Hostname</string>
<string name="failed_to_save">Failed to save</string>
Expand Down
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.26.2
require (
github.com/tailscale/wireguard-go v0.0.0-20260304043104-4184faf59e56
golang.org/x/mobile v0.0.0-20240806205939-81131f6468ab
tailscale.com v1.97.0-pre.0.20260408011054-a182b864ace4
tailscale.com v1.97.0-pre.0.20260420203310-1e68a11721fd
)

require (
Expand Down Expand Up @@ -61,9 +61,9 @@ require (
github.com/pires/go-proxyproto v0.8.1 // indirect
github.com/prometheus-community/pro-bing v0.4.0 // indirect
github.com/safchain/ethtool v0.3.0 // indirect
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect
github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d // indirect
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd // indirect
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 // indirect
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect
Expand Down
12 changes: 6 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -157,16 +157,16 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/studio-b12/gowebdav v0.9.0 h1:1j1sc9gQnNxbXXM4M/CebPOX4aXYtr7MojAVcN4dHjU=
github.com/studio-b12/gowebdav v0.9.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP5LWHEY//SWsYkSO3RWRZo4OSWagh3YD2vQ=
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4=
github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d h1:JcGKBZAL7ePLwOhUdN8qGQZlP5GueEiIZwY7R62pejE=
github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4=
github.com/tailscale/gliderssh v0.3.4-0.20260330083525-c1389c70ff89 h1:glgVc1ZYMjwN1Q/ITWeuSQyl029uayagaR2sjsifehc=
github.com/tailscale/gliderssh v0.3.4-0.20260330083525-c1389c70ff89/go.mod h1:wn16Km1EZOX4UEAyaZa3dBwfFGOJ7neck40NcwosJUw=
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869 h1:SRL6irQkKGQKKLzvQP/ke/2ZuB7Py5+XuqtOgSj+iMM=
github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd h1:Rf9uhF1+VJ7ZHqxrG8pJ6YacmHvVCmByDmGbAWCc/gA=
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd/go.mod h1:EbW0wDK/qEUYI0A5bqq0C2kF8JTQwWONmGDBbzsxxHo=
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU=
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA=
Expand Down Expand Up @@ -247,5 +247,5 @@ howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=
software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
tailscale.com v1.97.0-pre.0.20260408011054-a182b864ace4 h1:EbBRCAjxEWU/61I/DDqMg4aMKw0uqYCtt8diNQ3DQjI=
tailscale.com v1.97.0-pre.0.20260408011054-a182b864ace4/go.mod h1:J3yUifgjBmMBIylCvle8qVui/MDlsjP6aRPsZ9pjfUY=
tailscale.com v1.97.0-pre.0.20260420203310-1e68a11721fd h1:D8jEUQOnUrOHF3D6hFXwApugaKZvjL2eaedZ02IgSA4=
tailscale.com v1.97.0-pre.0.20260420203310-1e68a11721fd/go.mod h1:/8b6sKYTiJBP0X+uEOnjwYI0SnemeggQnOiRXi1MQbM=
8 changes: 7 additions & 1 deletion libtailscale/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ type App struct {
backend *ipnlocal.LocalBackend
ready sync.WaitGroup
backendMu sync.Mutex

// logger is the logtail logger whose uploads follow the user's
// IsClientLoggingEnabled preference. Populated once runBackend wires
// up the backend; nil before then.
logger atomic.Pointer[logtail.Logger]
}

func start(dataDir, directFileRoot string, hwAttestationPref bool, appCtx AppContext) Application {
Expand Down Expand Up @@ -142,6 +147,7 @@ func (a *App) runBackend(ctx context.Context, hardwareAttestation bool) error {
return err
}
a.logIDPublicAtomic.Store(&b.logIDPublic)
a.logger.Store(b.logger)
a.backend = b.backend
if hardwareAttestation {
a.backend.SetHardwareAttested()
Expand Down Expand Up @@ -329,7 +335,7 @@ func (a *App) newBackend(dataDir string, appCtx AppContext, store *stateStore,
log.Printf("netmon.New: %v", err)
}
b.netMon = netMon
b.setupLogs(dataDir, logID, logf, sys.HealthTracker.Get())
b.setupLogs(dataDir, logID, logf, sys.HealthTracker.Get(), a.isClientLoggingEnabled())
dialer := new(tsdial.Dialer)
vf := &VPNFacade{
SetBoth: b.setCfg,
Expand Down
7 changes: 7 additions & 0 deletions libtailscale/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,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)

// GetInterfacesAsJson gets a JSON representation of all network
// interfaces.
GetInterfacesAsJson() (string, error)
Expand Down Expand Up @@ -138,6 +141,10 @@ type Application interface {
// so it can re-read it via the [syspolicyHandler].
NotifyPolicyChanged()

// SetClientLoggingEnabled sets whether diagnostic logs are uploaded to
// Tailscale's logging backend. Changes take effect immediately.
SetClientLoggingEnabled(enabled bool)

// WatchNotifications provides a mechanism for subscribing to ipn.Notify
// updates. The given NotificationCallback's OnNotify function is invoked
// on every new ipn.Notify message. The returned NotificationManager
Expand Down
6 changes: 6 additions & 0 deletions libtailscale/localapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ func (app *App) NotifyPolicyChanged() {
app.policyStore.notifyChanged()
}

func (app *App) SetClientLoggingEnabled(enabled bool) {
if lg := app.logger.Load(); lg != nil {
lg.SetEnabled(enabled)
}
}

func (app *App) EditPrefs(prefs ipn.MaskedPrefs) (LocalAPIResponse, error) {
r, w := io.Pipe()
go func() {
Expand Down
17 changes: 16 additions & 1 deletion libtailscale/tailscale.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,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")
}
Expand All @@ -122,6 +130,10 @@ func (b *backend) setupLogs(logDir string, logID logid.PrivateID, logf logger.Lo
IncludeProcSequence: true,
HTTPC: &http.Client{Transport: transport},
CompressLogs: true,
// Start the logger disabled if the user opted out, so not even
// the internal "logtail started" banner reaches the server. The
// SetClientLoggingEnabled path flips this at runtime.
Disabled: !enableUpload,
}
logcfg.FlushDelayFn = func() time.Duration { return 2 * time.Minute }

Expand All @@ -136,6 +148,9 @@ func (b *backend) setupLogs(logDir string, logID logid.PrivateID, logf logger.Lo
}

b.logger = logtail.NewLogger(logcfg, logf)
if !enableUpload {
log.Printf("remote log upload disabled by user preference")
}

log.SetFlags(0)
log.SetOutput(b.logger)
Expand Down
Loading