fix(android): app splitting ONLY mode — don't mix allowed/disallowed apps

This commit is contained in:
dazzling-no-more
2026-04-24 23:16:26 +04:00
parent 7c102b4f63
commit 4ccd864e72
@@ -160,38 +160,39 @@ class MhrvVpnService : VpnService() {
.addRoute("0.0.0.0", 0) .addRoute("0.0.0.0", 0)
.addDnsServer("1.1.1.1") .addDnsServer("1.1.1.1")
.setBlocking(false) .setBlocking(false)
try {
builder.addDisallowedApplication(packageName)
} catch (e: Throwable) {
// Shouldn't happen for our own package, but don't hard-fail.
Log.w(TAG, "addDisallowedApplication failed: ${e.message}")
}
// Apply user-chosen app splitting on top of the mandatory // Apply user-chosen app splitting. The VpnService API treats
// self-exclusion above. // addAllowedApplication and addDisallowedApplication as mutually
// exclusive — calling both on one Builder throws
// IllegalArgumentException at establish() time, which is the bug
// that manifested as "ONLY mode tunnels everything" (establish()
// failed silently and the fallback never routed correctly).
// //
// ALL — no extra restriction; every other app routes through // ALL / EXCEPT: add the mandatory self-exclude (packageName) via
// us. Matches pre-splitting behaviour. // addDisallowedApplication so our own proxy's outbound to
// ONLY — allow-list. addAllowedApplication() for each chosen // google_ip doesn't loop through the TUN.
// package; anything missing from the list bypasses the // ONLY: self-exclusion is implicit — we're not in the allow-list.
// VPN on the OS-native route. Note that ONLY and the
// mandatory self-exclude are mutually exclusive in the
// VpnService API, so if the user also put us in the
// allow-list we skip the self-exclude (it's already
// implicit via "we're not in the list").
// EXCEPT — deny-list. addDisallowedApplication() for each chosen
// package, additive with our self-exclude.
// //
// Packages that are not installed (leftover selections from a // Packages that are not installed (leftover selections from a
// previous device) throw PackageManager.NameNotFoundException — // previous device) throw PackageManager.NameNotFoundException —
// we log and skip rather than aborting the whole VPN start. // we log and skip rather than aborting the whole VPN start.
when (cfg.splitMode) { when (cfg.splitMode) {
SplitMode.ALL -> { /* no-op */ } SplitMode.ALL -> {
try {
builder.addDisallowedApplication(packageName)
} catch (e: Throwable) {
Log.w(TAG, "addDisallowedApplication(self) failed: ${e.message}")
}
}
SplitMode.ONLY -> { SplitMode.ONLY -> {
if (cfg.splitApps.isEmpty()) { if (cfg.splitApps.isEmpty()) {
Log.w(TAG, "ONLY mode with empty splitApps list — no app would get the VPN; falling back to ALL") Log.w(TAG, "ONLY mode with empty splitApps list — no app would get the VPN; falling back to ALL")
try {
builder.addDisallowedApplication(packageName)
} catch (_: Throwable) {}
} else { } else {
for (pkg in cfg.splitApps) { for (pkg in cfg.splitApps) {
if (pkg == packageName) continue // can't tunnel ourselves
try { builder.addAllowedApplication(pkg) } catch (e: Throwable) { try { builder.addAllowedApplication(pkg) } catch (e: Throwable) {
Log.w(TAG, "addAllowedApplication($pkg) failed: ${e.message}") Log.w(TAG, "addAllowedApplication($pkg) failed: ${e.message}")
} }
@@ -199,6 +200,11 @@ class MhrvVpnService : VpnService() {
} }
} }
SplitMode.EXCEPT -> { SplitMode.EXCEPT -> {
try {
builder.addDisallowedApplication(packageName)
} catch (e: Throwable) {
Log.w(TAG, "addDisallowedApplication(self) failed: ${e.message}")
}
for (pkg in cfg.splitApps) { for (pkg in cfg.splitApps) {
if (pkg == packageName) continue // already self-excluded above if (pkg == packageName) continue // already self-excluded above
try { builder.addDisallowedApplication(pkg) } catch (e: Throwable) { try { builder.addDisallowedApplication(pkg) } catch (e: Throwable) {