From 69e02a93955879c6956953eee571a576e8e63f30 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 09:43:28 +0000 Subject: [PATCH] Stop marking unmetered networks as metered while the VPN runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Android defaults a VPN network to metered, so the metered flag has to be set explicitly. We passed a snapshot of Util.isMeteredNetwork(), which is wrong twice over: - ConnectivityManager.isActiveNetworkMetered() returns true when there is no active network at all, so a tunnel established before Wi-Fi associates (boot start, always-on VPN) is born metered; - the flag only reaches apps through establish(), and Builder.equals() never compared it, so a metered reload took the "Native restart" shortcut and the stale value survived for the life of the tunnel. Apps that ask the system whether the current network is metered — K-9 Mail, Syncthing — then hold back on any Wi-Fi for as long as TC runs (#959). Pass false instead, which does not force the network unmetered: it tells the platform to inherit meteredness from the underlying networks, so the VPN tracks the physical network as it changes, with no snapshot to go stale. Also track the flag in Builder and compare it, so a future change of the value does force a real re-establish (part of #763). Closes #959 Claude-Session: https://claude.ai/code/session_01184pQBPQMMwWef1H7DmgJA --- .../eu/faircode/netguard/ServiceSinkhole.java | 34 ++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java b/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java index e0e09f221..fc8604a14 100644 --- a/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java +++ b/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java @@ -1852,10 +1852,17 @@ private Builder getBuilder(List listAllowed, List listRule) { Builder builder = new Builder(); builder.setSession(getString(R.string.app_name)); - // Match the physical network's metered status so apps behave the - // same as without the VPN. (Android defaults VPNs to metered.) + // Android defaults VPNs to metered, which makes every app on the device + // treat an unmetered Wi-Fi as metered while the VPN runs (#959). Passing + // false does not force the network unmetered: it tells the platform to + // inherit meteredness from the underlying networks, so the VPN tracks + // the physical network as it changes. Snapshotting isMeteredNetwork() + // here cannot do that — the value is read once per establish, is true + // whenever there is no active network yet (a boot or always-on start + // before Wi-Fi associates), and then never reaches the live interface + // again while the tunnel lives. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) - builder.setMetered(Util.isMeteredNetwork(this)); + builder.setMetered(false); // Use blocking I/O on the TUN file descriptor for CPU efficiency // (avoids polling when there is no traffic) @@ -2107,8 +2114,10 @@ private Builder getBlockingBuilder(List listRule) { builder.setSession(getString(R.string.app_name)); builder.setBlocking(true); builder.setMtu(1280); + // Inherit meteredness from the underlying networks, as getBuilder does: + // the placeholder must not flip apps to metered mid-replacement. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) - builder.setMetered(Util.isMeteredNetwork(this)); + builder.setMetered(false); builder.addAddress("192.0.2.1", 32); builder.addRoute("0.0.0.0", 0); @@ -4811,6 +4820,7 @@ private class Builder extends VpnService.Builder { private Network activeNetwork; private NetworkInfo networkInfo; private int mtu; + private boolean metered = true; private List listAddress = new ArrayList<>(); private List listRoute = new ArrayList<>(); private List listDns = new ArrayList<>(); @@ -4823,6 +4833,19 @@ private Builder() { networkInfo = cm.getActiveNetworkInfo(); } + /** + * Kept in the comparison below so a changed metered flag forces a real + * re-establish instead of the "Native restart" shortcut (#763): the flag + * only reaches apps through {@link VpnService.Builder#establish()}. + */ + @RequiresApi(api = Build.VERSION_CODES.Q) + @Override + public VpnService.Builder setMetered(boolean metered) { + this.metered = metered; + super.setMetered(metered); + return this; + } + @Override public VpnService.Builder setMtu(int mtu) { this.mtu = mtu; @@ -4890,6 +4913,9 @@ public boolean equals(Object obj) { if (this.mtu != other.mtu) return false; + if (this.metered != other.metered) + return false; + if (this.listAddress.size() != other.listAddress.size()) return false;