Skip to content

Commit ede2a2d

Browse files
authored
android: add selection of included/excluded apps in split tunneling (#621)
* Add ability to select included/excluded apps from App Split Tunneling Updates tailscale/tailscale#14660 Signed-off-by: davfsa <davfsa@gmail.com> * Update app split tunneling description string Signed-off-by: davfsa <davfsa@gmail.com> * Address review comments Signed-off-by: davfsa <davfsa@gmail.com> * fix: update uses of excludedPackageNames Signed-off-by: davfsa <davfsa@gmail.com> * chore: pr review Do not add self package by default Signed-off-by: davfsa <davfsa@gmail.com> * feat: improve switch dialogs + add lazy app list loading Signed-off-by: davfsa <davfsa@gmail.com> --------- Signed-off-by: davfsa <davfsa@gmail.com>
1 parent 80320dc commit ede2a2d

7 files changed

Lines changed: 311 additions & 94 deletions

File tree

android/src/main/java/com/tailscale/ipn/App.kt

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,15 @@ open class UninitializedApp : Application() {
450450
// Key for shared preference that tracks whether or not we're able to start
451451
// the VPN (i.e. we're logged in and machine is authorized).
452452
private const val ABLE_TO_START_VPN_KEY = "ableToStartVPN"
453-
private const val DISALLOWED_APPS_KEY = "disallowedApps"
453+
454+
// The value is 'disallowedApps' as it used to represent
455+
// only disallowed applications. This has been changed
456+
// and allowing/disallowing is based on ALLOW_SELECTED_APPS_KEY
457+
//
458+
// The value is kept the same to not reset everyone's configuration
459+
private const val SELECTED_APPS_KEY = "disallowedApps"
460+
private const val ALLOW_SELECTED_APPS_KEY = "allowSelectedApps"
461+
454462
// File for shared preferences that are not encrypted.
455463
private const val UNENCRYPTED_PREFERENCES = "unencrypted"
456464
private lateinit var appInstance: UninitializedApp
@@ -615,25 +623,34 @@ open class UninitializedApp : Application() {
615623
return builder.build()
616624
}
617625

618-
fun updateUserDisallowedPackageNames(packageNames: List<String>) {
626+
fun updateUserSelectedPackages(packageNames: List<String>) {
619627
if (packageNames.any { it.isEmpty() }) {
620-
TSLog.e(TAG, "updateUserDisallowedPackageNames called with empty packageName(s)")
628+
TSLog.e(TAG, "updateUserSelectedPackage called with empty packageName(s)")
621629
return
622630
}
623-
getUnencryptedPrefs().edit().putStringSet(DISALLOWED_APPS_KEY, packageNames.toSet()).apply()
631+
632+
getUnencryptedPrefs().edit().putStringSet(SELECTED_APPS_KEY, packageNames.toSet()).apply()
633+
624634
this.restartVPN()
625635
}
626636

627-
fun disallowedPackageNames(): List<String> {
628-
val mdmDisallowed =
629-
MDMSettings.excludedPackages.flow.value.value?.split(",")?.map { it.trim() } ?: emptyList()
630-
if (mdmDisallowed.isNotEmpty()) {
631-
TSLog.d(TAG, "Excluded application packages were set via MDM: $mdmDisallowed")
632-
return builtInDisallowedPackageNames + mdmDisallowed
633-
}
634-
val userDisallowed =
635-
getUnencryptedPrefs().getStringSet(DISALLOWED_APPS_KEY, emptySet())?.toList() ?: emptyList()
636-
return builtInDisallowedPackageNames + userDisallowed
637+
fun switchUserSelectedPackages() {
638+
getUnencryptedPrefs()
639+
.edit()
640+
.putBoolean(ALLOW_SELECTED_APPS_KEY, !allowSelectedPackages())
641+
.apply()
642+
getUnencryptedPrefs().edit().putStringSet(SELECTED_APPS_KEY, setOf()).apply()
643+
644+
this.restartVPN()
645+
}
646+
647+
fun selectedPackageNames(): List<String> {
648+
return getUnencryptedPrefs().getStringSet(SELECTED_APPS_KEY, emptySet())?.toList()
649+
?: emptyList()
650+
}
651+
652+
fun allowSelectedPackages(): Boolean {
653+
return getUnencryptedPrefs().getBoolean(ALLOW_SELECTED_APPS_KEY, false)
637654
}
638655

639656
fun getAppScopedViewModel(): AppViewModel {

android/src/main/java/com/tailscale/ipn/IPNService.kt

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -141,11 +141,19 @@ open class IPNService : VpnService(), libtailscale.IPNService {
141141
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
142142
}
143143

144+
private fun allowApp(b: Builder, name: String) {
145+
try {
146+
b.addAllowedApplication(name)
147+
} catch (e: PackageManager.NameNotFoundException) {
148+
TSLog.e(TAG, "Failed to add allowed application: $e")
149+
}
150+
}
151+
144152
private fun disallowApp(b: Builder, name: String) {
145153
try {
146154
b.addDisallowedApplication(name)
147155
} catch (e: PackageManager.NameNotFoundException) {
148-
TSLog.d(TAG, "Failed to add disallowed application: $e")
156+
TSLog.e(TAG, "Failed to add disallowed application: $e")
149157
}
150158
}
151159

@@ -160,23 +168,45 @@ open class IPNService : VpnService(), libtailscale.IPNService {
160168
}
161169
b.setUnderlyingNetworks(null) // Use all available networks.
162170

163-
val includedPackages: List<String> =
171+
val mdmAllowed =
164172
MDMSettings.includedPackages.flow.value.value?.split(",")?.map { it.trim() } ?: emptyList()
165-
if (includedPackages.isNotEmpty()) {
166-
// If an admin defined a list of packages that are exclusively allowed to be used via
167-
// Tailscale,
168-
// then only allow those apps.
169-
for (packageName in includedPackages) {
173+
val mdmDisallowed =
174+
MDMSettings.excludedPackages.flow.value.value?.split(",")?.map { it.trim() } ?: emptyList()
175+
176+
var packagesList: List<String>
177+
var allowPackages: Boolean
178+
if (mdmAllowed.isNotEmpty()) {
179+
// An admin defined a list of packages that are exclusively allowed to be used via
180+
// Tailscale, so only allow those.
181+
packagesList = mdmAllowed
182+
allowPackages = true
183+
TSLog.d(TAG, "Included application packages were set via MDM: $mdmAllowed")
184+
} else if (mdmDisallowed.isNotEmpty()) {
185+
// An admin defined a list of packages that are excluded from accessing Tailscale,
186+
// so ignore user definitions and only exclude those
187+
packagesList = mdmDisallowed
188+
allowPackages = false
189+
TSLog.d(TAG, "Excluded application packages were set via MDM: $mdmDisallowed")
190+
} else {
191+
// Otherwise, prevent user manually disallowed apps from getting their traffic + DNS routed
192+
// via Tailscale
193+
packagesList = UninitializedApp.get().selectedPackageNames()
194+
allowPackages = UninitializedApp.get().allowSelectedPackages()
195+
TSLog.d(TAG, "Application packages were set by user: $packagesList")
196+
}
197+
198+
if (allowPackages) {
199+
for (packageName in packagesList) {
170200
TSLog.d(TAG, "Including app: $packageName")
171-
b.addAllowedApplication(packageName)
201+
allowApp(b, packageName)
172202
}
173203
} else {
174-
// Otherwise, prevent certain apps from getting their traffic + DNS routed via Tailscale:
175-
// - any app that the user manually disallowed in the GUI
176-
// - any app that we disallowed via hard-coding
177-
for (disallowedPackageName in UninitializedApp.get().disallowedPackageNames()) {
178-
TSLog.d(TAG, "Disallowing app: $disallowedPackageName")
179-
disallowApp(b, disallowedPackageName)
204+
// Make sure to also exclude hard-coded apps that are known to cause issues
205+
packagesList += UninitializedApp.get().builtInDisallowedPackageNames
206+
207+
for (packageName in packagesList) {
208+
TSLog.d(TAG, "Disallowing app: $packageName")
209+
disallowApp(b, packageName)
180210
}
181211
}
182212

android/src/main/java/com/tailscale/ipn/ui/util/InstalledAppsManager.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package com.tailscale.ipn.ui.util
66
import android.Manifest
77
import android.content.pm.ApplicationInfo
88
import android.content.pm.PackageManager
9+
import com.tailscale.ipn.BuildConfig
910

1011
data class InstalledApp(val name: String, val packageName: String)
1112

@@ -26,7 +27,7 @@ class InstalledAppsManager(
2627
}
2728

2829
private val appIsIncluded: (ApplicationInfo) -> Boolean = { app ->
29-
app.packageName != "com.tailscale.ipn" &&
30+
app.packageName != BuildConfig.APPLICATION_ID &&
3031
// Only show apps that can access the Internet
3132
packageManager.checkPermission(Manifest.permission.INTERNET, app.packageName) ==
3233
PackageManager.PERMISSION_GRANTED

android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ fun SettingsView(
8989
Lists.ItemDivider()
9090
Setting.Text(
9191
R.string.split_tunneling,
92-
subtitle = stringResource(R.string.exclude_certain_apps_from_using_tailscale),
92+
subtitle = stringResource(R.string.filter_apps_allowed_to_access_tailscale),
9393
onClick = settingsNav.onNavigateToSplitTunneling)
9494

9595
if (showTailnetLock.value == ShowHide.Show) {

0 commit comments

Comments
 (0)