Skip to content
Open
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
26 changes: 24 additions & 2 deletions android/src/main/java/com/tailscale/ipn/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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://")) {
Expand All @@ -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,
Expand Down Expand Up @@ -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<NetworkInterface> =
java.util.Collections.list(NetworkInterface.getNetworkInterfaces())
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<String>) {
if (packageNames.any { it.isEmpty() }) {
TSLog.e(TAG, "updateUserDisallowedPackageNames called with empty packageName(s)")
Expand Down
14 changes: 12 additions & 2 deletions android/src/main/java/com/tailscale/ipn/mdm/MDMSettings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
}
24 changes: 24 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 @@ -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 = {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 = {}
Expand All @@ -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)
})
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)
}
}
3 changes: 3 additions & 0 deletions android/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,9 @@
<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 logs are uploaded to Tailscale support. When disabled no support or network flow logs.\nChanges require restarting the app to take effect.</string>
<string name="client_remote_logging_enabled_subtitle_mdm">Client logging is always enabled for devices under remote management.</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
2 changes: 1 addition & 1 deletion libtailscale/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions libtailscale/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
15 changes: 14 additions & 1 deletion libtailscale/tailscale.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down Expand Up @@ -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)

Expand Down