mdm: support split tunneling configuration via syspolicy (#441)

Updates tailscale/tailscale#6912

Adds two new Android-only MDM policies: IncludedPackageNames and ExcludedPackageNames. These are comma-separated string values that contain Android package names to configure app-based split tunneling programmatically.

If ExcludedPackageNames is non-empty, Tailscale will exclude the given apps from the VPN tunnel.

If IncludedPackageNames is non-empty, Tailscale will configure the VPN tunnel to only route the given apps via Tailscale.

Signed-off-by: Andrea Gottardo <andrea@gottardo.me>
pull/447/head
Andrea Gottardo 4 months ago committed by GitHub
parent 65a025007f
commit c4a1dec8eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -510,6 +510,11 @@ open class UninitializedApp : Application() {
}
fun disallowedPackageNames(): List<String> {
val mdmDisallowed = MDMSettings.excludedPackages.flow.value?.split(",")?.map { it.trim() } ?: emptyList()
if (mdmDisallowed.isNotEmpty()) {
Log.d(TAG, "Excluded application packages were set via MDM: $mdmDisallowed")
return builtInDisallowedPackageNames + mdmDisallowed
}
val userDisallowed =
getUnencryptedPrefs().getStringSet(DISALLOWED_APPS_KEY, emptySet())?.toList() ?: emptyList()
return builtInDisallowedPackageNames + userDisallowed

@ -9,6 +9,7 @@ import android.net.VpnService
import android.os.Build
import android.system.OsConstants
import android.util.Log
import com.tailscale.ipn.mdm.MDMSettings
import libtailscale.Libtailscale
import java.util.UUID
@ -114,13 +115,25 @@ open class IPNService : VpnService(), libtailscale.IPNService {
}
b.setUnderlyingNetworks(null) // Use all available networks.
// Prevent certain apps from getting their traffic + DNS routed via Tailscale:
val includedPackages: List<String> =
MDMSettings.includedPackages.flow.value?.split(",")?.map { it.trim() } ?: emptyList()
if (includedPackages.isNotEmpty()) {
// If an admin defined a list of packages that are exclusively allowed to be used via
// Tailscale,
// then only allow those apps.
for (packageName in includedPackages) {
Log.d(TAG, "Including app: $packageName")
b.addAllowedApplication(packageName)
}
} else {
// Otherwise, prevent certain apps from getting their traffic + DNS routed via Tailscale:
// - any app that the user manually disallowed in the GUI
// - any app that we disallowed via hard-coding
for (disallowedPackageName in UninitializedApp.get().disallowedPackageNames()) {
Log.d(TAG, "Disallowing app: $disallowedPackageName")
disallowApp(b, disallowedPackageName)
}
}
return VPNServiceBuilder(b)
}

@ -83,6 +83,12 @@ object MDMSettings {
val allowedSuggestedExitNodes =
StringArrayListMDMSetting("AllowedSuggestedExitNodes", "Allowed Suggested Exit Nodes")
// Allows admins to define a list of packages that won't be routed via Tailscale.
val excludedPackages = StringMDMSetting("ExcludedPackageNames", "Excluded Package Names")
// Allows admins to define a list of packages that will be routed via Tailscale, letting all other
// apps skip the VPN tunnel.
val includedPackages = StringMDMSetting("IncludedPackageNames", "Included Package Names")
val allSettings by lazy {
MDMSettings::class
.declaredMemberProperties

@ -37,6 +37,8 @@ fun SplitTunnelAppPickerView(
val installedApps by model.installedApps.collectAsState()
val excludedPackageNames by model.excludedPackageNames.collectAsState()
val builtInDisallowedPackageNames: List<String> = App.get().builtInDisallowedPackageNames
val mdmIncludedPackages by model.mdmIncludedPackages.collectAsState()
val mdmExcludedPackages by model.mdmExcludedPackages.collectAsState()
Scaffold(topBar = { Header(titleRes = R.string.split_tunneling, onBack = backToSettings) }) {
innerPadding ->
@ -50,6 +52,21 @@ fun SplitTunnelAppPickerView(
.selected_apps_will_access_the_internet_directly_without_using_tailscale))
})
}
if (mdmExcludedPackages?.isNotEmpty() == true) {
item("mdmExcludedNotice") {
ListItem(
headlineContent = {
Text(stringResource(R.string.certain_apps_are_not_routed_via_tailscale))
})
}
} else if (mdmIncludedPackages?.isNotEmpty() == true) {
item("mdmIncludedNotice") {
ListItem(
headlineContent = {
Text(stringResource(R.string.only_specific_apps_are_routed_via_tailscale))
})
}
} else {
item("resolversHeader") {
Lists.SectionDivider(
stringResource(R.string.count_excluded_apps, excludedPackageNames.count()))
@ -90,4 +107,5 @@ fun SplitTunnelAppPickerView(
}
}
}
}
}

@ -5,6 +5,7 @@ package com.tailscale.ipn.ui.viewModel
import androidx.lifecycle.ViewModel
import com.tailscale.ipn.App
import com.tailscale.ipn.mdm.MDMSettings
import com.tailscale.ipn.ui.util.InstalledApp
import com.tailscale.ipn.ui.util.InstalledAppsManager
import com.tailscale.ipn.ui.util.set
@ -15,13 +16,15 @@ class SplitTunnelAppPickerViewModel : ViewModel() {
val installedAppsManager = InstalledAppsManager(packageManager = App.get().packageManager)
val excludedPackageNames: StateFlow<List<String>> = MutableStateFlow(listOf())
val installedApps: StateFlow<List<InstalledApp>> = MutableStateFlow(listOf())
val mdmExcludedPackages: StateFlow<String?> = MDMSettings.excludedPackages.flow
val mdmIncludedPackages: StateFlow<String?> = MDMSettings.includedPackages.flow
init {
installedApps.set(installedAppsManager.fetchInstalledApps())
excludedPackageNames.set(
App.get()
.disallowedPackageNames()
.intersect(installedApps.value.map { it.packageName })
.intersect(installedApps.value.map { it.packageName }.toSet())
.toList())
}

@ -277,4 +277,10 @@
<string name="exclude_certain_apps_from_using_tailscale">Exclude certain apps from using Tailscale</string>
<string name="selected_apps_will_access_the_internet_directly_without_using_tailscale">Apps selected here will access the Internet directly, without using Tailscale.</string>
<string name="count_excluded_apps">Excluded apps (%1$s)</string>
<string name="certain_apps_are_not_routed_via_tailscale">Certain apps are not routed via Tailscale on this device. This setting is managed by your organization and cannot be changed by you. For more information, contact your network administrator.</string>
<string name="only_specific_apps_are_routed_via_tailscale">Only specific apps are routed via Tailscale on this device. This setting is managed by your organization and cannot be changed by you. For more information, contact your network administrator.</string>
<string name="specifies_a_list_of_apps_that_will_be_excluded_from_tailscale_routes_and_dns_even_when_tailscale_is_running_all_other_apps_will_use_tailscale">Specifies a list of apps that will be excluded from Tailscale routes and DNS even when Tailscale is running. All other apps will use Tailscale.</string>
<string name="specifies_a_list_of_apps_that_will_always_use_tailscale_routes_and_dns_when_tailscale_is_running_all_other_apps_won_t_use_tailscale_if_this_value_is_non_empty">Specifies a list of apps that will always use Tailscale routes and DNS when Tailscale is running. All other apps won\'t use Tailscale if this value is non-empty.</string>
<string name="included_packages">Included packages</string>
<string name="excluded_packages">Excluded packages</string>
</resources>

@ -110,4 +110,16 @@
android:key="RunExitNode"
android:restrictionType="choice"
android:title="@string/run_as_exit_node_visibility" />
<restriction
android:description="@string/specifies_a_list_of_apps_that_will_be_excluded_from_tailscale_routes_and_dns_even_when_tailscale_is_running_all_other_apps_will_use_tailscale"
android:key="ExcludedPackageNames"
android:restrictionType="string"
android:title="@string/excluded_packages" />
<restriction
android:description="@string/specifies_a_list_of_apps_that_will_always_use_tailscale_routes_and_dns_when_tailscale_is_running_all_other_apps_won_t_use_tailscale_if_this_value_is_non_empty"
android:key="IncludedPackageNames"
android:restrictionType="string"
android:title="@string/included_packages" />
</restrictions>
Loading…
Cancel
Save