From 878cb694f37bd00ef062b8f871a4a93a82a00d3a Mon Sep 17 00:00:00 2001 From: fengzzyun Date: Sun, 3 May 2026 16:07:36 +0800 Subject: [PATCH] android: fix include-mode DNS by inverting to exclude strategy Include mode currently uses VpnService.Builder.addAllowedApplication(), which sets DNS servers globally. Non-VPN apps try to reach the VPN DNS (100.x.x.x) through the physical interface, which is unreachable, causing DNS resolution failures. Instead of using addAllowedApplication(), collect all installed packages, subtract the user-selected ones, and use addDisallowedApplication() for the remainder. This leverages Android's well-tested per-app VPN exclude path, which correctly routes DNS to the physical network for non-VPN apps. Updates tailscale/tailscale#13816 Signed-off-by: fengzzyun --- .../main/java/com/tailscale/ipn/IPNService.kt | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/IPNService.kt b/android/src/main/java/com/tailscale/ipn/IPNService.kt index 848092f8c8..d51c6effb6 100644 --- a/android/src/main/java/com/tailscale/ipn/IPNService.kt +++ b/android/src/main/java/com/tailscale/ipn/IPNService.kt @@ -210,9 +210,36 @@ open class IPNService : VpnService(), libtailscale.IPNService { } if (allowPackages) { - for (packageName in packagesList) { - TSLog.d(TAG, "Including app: $packageName") - allowApp(b, packageName) + TSLog.d(TAG, "Include mode enabled -- using inverted-exclude strategy") + + // Build the set of packages to DISALLOW (everything except user-selected ones) + val packagesToExclude = mutableSetOf() + + // 1. Get all installed apps + val allInstalled = packageManager.getInstalledApplications(0) + .mapNotNull { applicationInfo -> + try { applicationInfo.packageName } catch (_: Exception) { null } + } + .toSet() + TSLog.d(TAG, "Total installed packages: ${allInstalled.size}") + + // 2. Remove user-selected packages (these should go through the tunnel) + val userSelected = packagesList.toSet() + packagesToExclude.addAll(allInstalled - userSelected) + + // 3. Also exclude built-in disallowed packages (voicemail, etc.) + val builtinExcluded = UninitializedApp.get().builtInDisallowedPackageNames.toSet() + packagesToExclude.addAll(builtinExcluded) + + TSLog.d(TAG, "Inverted exclude: selected=${userSelected.size}, " + + "excluded=${packagesToExclude.size - builtinExcluded.size}, " + + "builtin=${builtinExcluded.size}") + + // 4. Disallow all apps not in the user's include list + // This avoids addAllowedApplication() which breaks DNS for non-VPN apps + for (packageName in packagesToExclude) { + TSLog.d(TAG, "Disallowing app (inverted include): $packageName") + disallowApp(b, packageName) } } else { // Make sure to also exclude hard-coded apps that are known to cause issues