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
71 changes: 67 additions & 4 deletions android/src/main/java/com/tailscale/ipn/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,11 @@ 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"
// File for shared preferences that are not encrypted.
private const val ALLOWED_APPS_KEY = "allowedApps"
private const val SPLIT_TUNNEL_KEY = "splitTunnelEnabled"
private const val SPLIT_TUNNEL_MODE_KEY = "split_tunnel_mode"

// File for shared preferences that are not encrypted.
private const val UNENCRYPTED_PREFERENCES = "unencrypted"
private lateinit var appInstance: UninitializedApp
lateinit var notificationManager: NotificationManagerCompat
Expand Down Expand Up @@ -599,19 +603,73 @@ open class UninitializedApp : Application() {
this.restartVPN()
}

fun disallowedPackageNames(): List<String> {
fun updateUserAllowedPackageNames(packageNames: List<String>) {
if (packageNames.any { it.isEmpty() }) {
TSLog.e(TAG, "updateUserAllowedPackageNames called with empty packageName(s)")
return
}
getUnencryptedPrefs().edit().putStringSet(ALLOWED_APPS_KEY, packageNames.toSet()).apply()
this.restartVPN()
}

fun isSplitTunnelEnabled(): Boolean =
getUnencryptedPrefs().getBoolean(SPLIT_TUNNEL_KEY, false)

fun setSplitTunnelEnabled(enabled: Boolean) {
getUnencryptedPrefs().edit().putBoolean(SPLIT_TUNNEL_KEY, enabled).apply()
restartVPN()
}

fun setSplitTunnelMode(mode: SplitTunnelMode) {
getUnencryptedPrefs().edit()
.putString(SPLIT_TUNNEL_MODE_KEY, mode.name)
.apply()
restartVPN()
}

fun getSplitTunnelMode(): SplitTunnelMode {
val stored = getUnencryptedPrefs().getString(SPLIT_TUNNEL_MODE_KEY, null)
return if (stored != null) {
SplitTunnelMode.valueOf(stored)
} else {
SplitTunnelMode.EXCLUDE
}
}




fun disallowedPackageNames(): List<String> {
val mdmDisallowed =
MDMSettings.excludedPackages.flow.value.value?.split(",")?.map { it.trim() } ?: emptyList()
if (mdmDisallowed.isNotEmpty()) {

if (mdmDisallowed.isNotEmpty()) {
TSLog.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
}

fun getAppScopedViewModel(): AppViewModel {

fun allowedPackageNames(): List<String> {
val mdmAllowed =
MDMSettings.includedPackages.flow.value.value?.split(",")?.map { it.trim() } ?: emptyList()

if (mdmAllowed.isNotEmpty()) {
TSLog.d(TAG, "Included application packages were set via MDM: $mdmAllowed")
return mdmAllowed
}

val userAllowed =
getUnencryptedPrefs().getStringSet(ALLOWED_APPS_KEY, emptySet())?.toList() ?: emptyList()
return userAllowed
}


fun getAppScopedViewModel(): AppViewModel {
return appViewModel
}

Expand Down Expand Up @@ -640,4 +698,9 @@ open class UninitializedApp : Application() {
// Android Connectivity Service https://github.com/tailscale/tailscale/issues/14128
"com.google.android.apps.scone",
)

enum class SplitTunnelMode {
INCLUDE,
EXCLUDE
}
}
105 changes: 74 additions & 31 deletions android/src/main/java/com/tailscale/ipn/IPNService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -149,39 +149,82 @@ open class IPNService : VpnService(), libtailscale.IPNService {
}
}

override fun newBuilder(): VPNServiceBuilder {
val b: Builder =
Builder()
.setConfigureIntent(configIntent())
.allowFamily(OsConstants.AF_INET)
.allowFamily(OsConstants.AF_INET6)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
b.setMetered(false) // Inherit the metered status from the underlying networks.
}
b.setUnderlyingNetworks(null) // Use all available networks.

val includedPackages: List<String> =
MDMSettings.includedPackages.flow.value.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) {
TSLog.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()) {
TSLog.d(TAG, "Disallowing app: $disallowedPackageName")
disallowApp(b, disallowedPackageName)
}
private fun allowApp(b: Builder, name: String) {
try {
b.addAllowedApplication(name)
} catch (e: PackageManager.NameNotFoundException) {
TSLog.d(TAG, "Failed to add allowed application: $e")
}
}

return VPNServiceBuilder(b)
}
override fun newBuilder(): VPNServiceBuilder {
val b: Builder =
Builder()
.setConfigureIntent(configIntent())
.allowFamily(OsConstants.AF_INET)
.allowFamily(OsConstants.AF_INET6)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
b.setMetered(false) // Inherit the metered status from the underlying networks.
}
b.setUnderlyingNetworks(null) // Use all available networks.

val app = UninitializedApp.get()

val mdmAllowed = MDMSettings.includedPackages.flow.value.value?.
split(",")?.map { it.trim() }?.filter { it.isNotEmpty() } ?: emptyList()
val mdmDisallowed = MDMSettings.excludedPackages.flow.value.value?.
split(",")?.map { it.trim() }?.filter { it.isNotEmpty() } ?: emptyList()

val splitEnabled = app.isSplitTunnelEnabled()
val mode = app.getSplitTunnelMode()

when {
mdmAllowed.isNotEmpty() -> {
TSLog.d(TAG, "MDM include mode, allowed = $mdmAllowed")
mdmAllowed.forEach { pkg ->
TSLog.d(TAG, "Including app via MDM: $pkg")
allowApp(b, pkg)
}
}

mdmDisallowed.isNotEmpty() -> {
val effectiveDisallowed = app.builtInDisallowedPackageNames + mdmDisallowed
TSLog.d(TAG, "MDM exclude mode, disallowed = $effectiveDisallowed")
effectiveDisallowed.forEach { pkg ->
TSLog.d(TAG, "Disallowing app via MDM: $pkg")
disallowApp(b, pkg)
}
}

!splitEnabled -> {
TSLog.d(TAG, "Split tunneling disabled; using built-in disallowed only")
app.builtInDisallowedPackageNames.forEach { pkg ->
TSLog.d(TAG, "Disallowing built-in app: $pkg")
disallowApp(b, pkg)
}
}

mode == UninitializedApp.SplitTunnelMode.INCLUDE -> {
val userAllowed = app.allowedPackageNames()
TSLog.d(TAG, "User INCLUDE mode; allowed = $userAllowed")
userAllowed.forEach { pkg ->
TSLog.d(TAG, "Including app via user INCLUDE: $pkg")
allowApp(b, pkg)
}
}

else -> {
val effectiveDisallowed = app.disallowedPackageNames()
TSLog.d(TAG, "User EXCLUDE mode; disallowed = $effectiveDisallowed")
effectiveDisallowed.forEach { pkg ->
TSLog.d(TAG, "Disallowing app via user EXCLUDE/built-in: $pkg")
disallowApp(b, pkg)
}
}
}

return VPNServiceBuilder(b)
}

companion object {
const val ACTION_START_VPN = "com.tailscale.ipn.START_VPN"
Expand Down
1 change: 1 addition & 0 deletions android/src/main/java/com/tailscale/ipn/ui/model/Ipn.kt
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ class Ipn {
CorpDNSSet = true
}


var ExitNodeID: StableNodeID? = null
set(value) {
field = value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ fun SettingsView(
Lists.ItemDivider()
Setting.Text(
R.string.split_tunneling,
subtitle = stringResource(R.string.exclude_certain_apps_from_using_tailscale),
subtitle = stringResource(R.string.include_or_exclude_certain_apps_from_using_tailscale),
onClick = settingsNav.onNavigateToSplitTunneling)

if (showTailnetLock.value == ShowHide.Show) {
Expand Down
Loading