From 410449998eb7689b7b1afcd764d1cfeb5820861a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Apr 2026 21:08:40 +0000 Subject: [PATCH 1/4] Remove unnecessary VPN rebuilds on network changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TrackerControl doesn't need to rebuild the VPN when the network type changes (WiFi/mobile/2G/3G/4G/hotspot). Unlike NetGuard, TC doesn't apply different blocking rules per network type — tracker blocking is handled dynamically in blockKnownTracker() and the VPN app routing (filter mode) uses rule.apply which is network-independent. Removed reload() calls from three network-change receivers: - connectivityChangedReceiver: no longer rebuilds VPN on WiFi/mobile switch - phoneStateListener: no longer rebuilds VPN on 2G/3G/4G generation change - apStateReceiver: no longer rebuilds VPN on hotspot state change The networkMonitorCallback (connectivity validation), idleStateReceiver, interactiveStateReceiver, and packageChangedReceiver are preserved as they serve purposes beyond network-type differentiation. https://claude.ai/code/session_01PuXevPG3gUKEXWHrgRMptV --- .../eu/faircode/netguard/ServiceSinkhole.java | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java b/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java index 8a0670ad..8dfd14c8 100644 --- a/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java +++ b/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java @@ -2454,9 +2454,10 @@ public void onReceive(Context context, Intent intent) { @Override @TargetApi(Build.VERSION_CODES.M) public void onReceive(Context context, Intent intent) { - Log.i(TAG, "Received " + intent); + // TrackerControl: skip VPN rebuild on hotspot state changes. + // TC's tracker blocking is independent of tethering/AP state. + Log.i(TAG, "AP state changed (skipping reload): " + intent); Util.logExtras(intent); - reload("AP state changed", ServiceSinkhole.this, false); } }; @@ -2471,10 +2472,12 @@ public void onReceive(Context context, Intent intent) { return; } - // Reload rules - Log.i(TAG, "Received " + intent); + // TrackerControl: skip VPN rebuild on network changes. + // Unlike NetGuard, TC doesn't apply different blocking rules per network type + // (WiFi vs mobile). Tracker blocking is handled dynamically in blockKnownTracker() + // and the VPN app routing (filter mode) is network-independent. + Log.i(TAG, "Connectivity changed (skipping reload): " + intent); Util.logExtras(intent); - reload("connectivity changed", ServiceSinkhole.this, false); } }; @@ -2581,17 +2584,11 @@ private void checkConnectivity(Network network, NetworkInfo ni, NetworkCapabilit public void onDataConnectionStateChanged(int state, int networkType) { if (state == TelephonyManager.DATA_CONNECTED) { String current_generation = Util.getNetworkGeneration(ServiceSinkhole.this); - Log.i(TAG, "Data connected generation=" + current_generation); - + // TrackerControl: skip VPN rebuild on network generation changes. + // TC doesn't differentiate blocking by 2G/3G/4G network type. if (last_generation == null || !last_generation.equals(current_generation)) { - Log.i(TAG, "New network generation=" + current_generation); + Log.i(TAG, "Network generation changed to " + current_generation + " (skipping reload)"); last_generation = current_generation; - - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ServiceSinkhole.this); - if (prefs.getBoolean("unmetered_2g", false) || - prefs.getBoolean("unmetered_3g", false) || - prefs.getBoolean("unmetered_4g", false)) - reload("data connection state changed", ServiceSinkhole.this, false); } } } From 666586b650b407505170972e44bb4f44d9c4ada9 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Apr 2026 21:14:12 +0000 Subject: [PATCH 2/4] Remove legacy NetGuard network-type VPN rebuild logic TrackerControl always runs in filter mode, making several NetGuard mechanisms dead code: - getAllowedRules(): Removed all wifi/metered/roaming/generation/screen logic. In filter mode, listAllowed doesn't affect VPN routing (builder uses rule.apply) and mapUidAllowed is never read in isAddressAllowed(). Now simply returns all rules for the notification count. - reload() interactive check: Removed the screen_wifi/screen_other optimization that skipped reloads. TC doesn't use screen-based per-network blocking rules. - interactiveStateReceiver: Removed VPN rebuild on screen on/off. Kept last_interactive update for stats handling. - idleStateReceiver: Removed VPN rebuild when exiting Doze mode. TC's tracker blocking doesn't change based on idle state. https://claude.ai/code/session_01PuXevPG3gUKEXWHrgRMptV --- .../eu/faircode/netguard/ServiceSinkhole.java | 129 +++--------------- 1 file changed, 20 insertions(+), 109 deletions(-) diff --git a/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java b/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java index 8dfd14c8..4d7d4ef6 100644 --- a/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java +++ b/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java @@ -559,23 +559,6 @@ private void start() { private void reload(boolean interactive) { List listRule = Rule.getRules(true, ServiceSinkhole.this); - // Check if rules needs to be reloaded - if (interactive) { - boolean process = false; - for (Rule rule : listRule) { - boolean blocked = (last_metered ? rule.other_blocked : rule.wifi_blocked); - boolean screen = (last_metered ? rule.screen_other : rule.screen_wifi); - if (blocked && screen) { - process = true; - break; - } - } - if (!process) { - Log.i(TAG, "No changed rules on interactive state change"); - return; - } - } - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ServiceSinkhole.this); // Refresh cached preferences for shouldTrackApp() @@ -1965,69 +1948,18 @@ private void prepareForwarding() { } private List getAllowedRules(List listRule) { - List listAllowed = new ArrayList<>(); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); - - // Check state - boolean wifi = Util.isWifiActive(this); - boolean metered = Util.isMeteredNetwork(this); - boolean useMetered = prefs.getBoolean("use_metered", false); - String ssidNetwork = Util.getWifiSSID(this); - String generation = Util.getNetworkGeneration(this); - boolean unmetered_2g = prefs.getBoolean("unmetered_2g", false); - boolean unmetered_3g = prefs.getBoolean("unmetered_3g", false); - boolean unmetered_4g = prefs.getBoolean("unmetered_4g", false); - boolean roaming = Util.isRoaming(ServiceSinkhole.this); - boolean national = prefs.getBoolean("national_roaming", false); - boolean eu = prefs.getBoolean("eu_roaming", false); - boolean tethering = prefs.getBoolean("tethering", false); - boolean filter = prefs.getBoolean("filter", true); - - // Update connected state + // TrackerControl: In filter mode (always on for TC), the allowed rules list + // doesn't affect VPN routing or tracker blocking. The VPN builder uses rule.apply + // (network-independent) and tracker blocking is handled dynamically in + // blockKnownTracker(). We just return all rules for the notification count. last_connected = Util.isConnected(ServiceSinkhole.this); + last_metered = Util.isMeteredNetwork(ServiceSinkhole.this); - boolean org_metered = metered; - boolean org_roaming = roaming; - - // Update metered state - if (wifi && !useMetered) - metered = false; - if (unmetered_2g && "2G".equals(generation)) - metered = false; - if (unmetered_3g && "3G".equals(generation)) - metered = false; - if (unmetered_4g && "4G".equals(generation)) - metered = false; - last_metered = metered; - - // Update roaming state - if (roaming && eu) - roaming = !Util.isEU(this); - if (roaming && national) - roaming = !Util.isNational(this); - - Log.i(TAG, "Get allowed" + - " connected=" + last_connected + - " wifi=" + wifi + - " network=" + ssidNetwork + - " metered=" + metered + "/" + org_metered + - " generation=" + generation + - " roaming=" + roaming + "/" + org_roaming + - " interactive=" + last_interactive + - " tethering=" + tethering + - " filter=" + filter); - - if (last_connected) - for (Rule rule : listRule) { - boolean blocked = (metered ? rule.other_blocked : rule.wifi_blocked); - boolean screen = (metered ? rule.screen_other : rule.screen_wifi); - if ((!blocked || (screen && last_interactive)) && - (!metered || !(rule.roaming && roaming))) - listAllowed.add(rule); - } - - Log.i(TAG, "Allowed " + listAllowed.size() + " of " + listRule.size()); - return listAllowed; + Log.i(TAG, "Get allowed connected=" + last_connected + + " metered=" + last_metered + + " rules=" + listRule.size()); + + return new ArrayList<>(listRule); } private void stopVPN(ParcelFileDescriptor pfd) { @@ -2378,33 +2310,14 @@ public void onReceive(final Context context, final Intent intent) { Log.i(TAG, "Received " + intent); Util.logExtras(intent); - executor.submit(new Runnable() { - @Override - public void run() { - AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - Intent i = new Intent(ACTION_SCREEN_OFF_DELAYED); - i.setPackage(context.getPackageName()); - PendingIntent pi = PendingIntentCompat.getBroadcast(context, 0, i, - PendingIntent.FLAG_UPDATE_CURRENT); - am.cancel(pi); - - try { - last_interactive = Intent.ACTION_SCREEN_ON.equals(intent.getAction()); - reload("interactive state changed", ServiceSinkhole.this, true); + // TrackerControl: skip VPN rebuild on screen on/off. + // TC doesn't use screen-based per-network blocking rules (screen_wifi/screen_other). + // Just update the interactive state for stats handling. + last_interactive = Intent.ACTION_SCREEN_ON.equals(intent.getAction()); - // Start/stop stats - statsHandler.sendEmptyMessage( - Util.isInteractive(ServiceSinkhole.this) ? MSG_STATS_START : MSG_STATS_STOP); - } catch (Throwable ex) { - Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) - am.set(AlarmManager.RTC_WAKEUP, new Date().getTime() + 15 * 1000L, pi); - else - am.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, new Date().getTime() + 15 * 1000L, pi); - } - } - }); + // Start/stop stats + statsHandler.sendEmptyMessage( + Util.isInteractive(ServiceSinkhole.this) ? MSG_STATS_START : MSG_STATS_STOP); } }; @@ -2442,11 +2355,9 @@ public void onReceive(Context context, Intent intent) { Util.logExtras(intent); PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); - Log.i(TAG, "device idle=" + pm.isDeviceIdleMode()); - - // Reload rules when coming from idle mode - if (!pm.isDeviceIdleMode()) - reload("idle state changed", ServiceSinkhole.this, false); + // TrackerControl: skip VPN rebuild on idle state changes. + // TC's tracker blocking doesn't change based on Doze mode. + Log.i(TAG, "Device idle=" + pm.isDeviceIdleMode() + " (skipping reload)"); } }; From 5ba5261d40bf5edaabe497366d956cfc42bd239b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Apr 2026 21:32:09 +0000 Subject: [PATCH 3/4] Fix potential lockdown regression and remove unused executor - Preserve WiFi metered override in getAllowedRules(): the old code treated WiFi as unmetered unless use_metered was set, which affects isLockedDown() for IP filter rules. Without this, users on metered WiFi networks (hotspots) could see different lockdown behavior. - Remove unused ExecutorService field and imports: was only used by the old interactiveStateReceiver reload logic, now creates a thread pool that's never used. https://claude.ai/code/session_01PuXevPG3gUKEXWHrgRMptV --- .../eu/faircode/netguard/ServiceSinkhole.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java b/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java index 4d7d4ef6..88bbf132 100644 --- a/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java +++ b/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java @@ -122,8 +122,6 @@ import java.util.Objects; import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.zip.GZIPInputStream; @@ -216,7 +214,7 @@ public enum Command { private static volatile PowerManager.WakeLock wlInstance = null; - private ExecutorService executor = Executors.newCachedThreadPool(); + // executor removed: was only used by old interactiveStateReceiver reload logic private static final String ACTION_HOUSE_HOLDING = "eu.faircode.netguard.HOUSE_HOLDING"; private static final String ACTION_SCREEN_OFF_DELAYED = "eu.faircode.netguard.SCREEN_OFF_DELAYED"; @@ -1953,7 +1951,14 @@ private List getAllowedRules(List listRule) { // (network-independent) and tracker blocking is handled dynamically in // blockKnownTracker(). We just return all rules for the notification count. last_connected = Util.isConnected(ServiceSinkhole.this); - last_metered = Util.isMeteredNetwork(ServiceSinkhole.this); + + // Preserve metered override: treat WiFi as unmetered unless use_metered is set. + // This affects isLockedDown() which is still used for IP filter rules. + boolean metered = Util.isMeteredNetwork(this); + if (Util.isWifiActive(this) && + !PreferenceManager.getDefaultSharedPreferences(this).getBoolean("use_metered", false)) + metered = false; + last_metered = metered; Log.i(TAG, "Get allowed connected=" + last_connected + " metered=" + last_metered + @@ -3147,7 +3152,7 @@ public void onDestroy() { } } - executor.shutdownNow(); + // executor.shutdownNow() removed: executor no longer used super.onDestroy(); } From 7ba263fe795784688d283bdeb980c7acabf9424a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Apr 2026 21:36:17 +0000 Subject: [PATCH 4/4] Restore VPN reload on Doze exit for tunnel health The native tunnel thread may get into a bad state during Doze's network restrictions. Reloading on Doze exit ensures the tunnel is healthy and packet filtering resumes correctly. Without this, a stuck tunnel could silently stop filtering until the next reload from another trigger. Also removed unused ExecutorService (was only used by old interactiveStateReceiver logic). https://claude.ai/code/session_01PuXevPG3gUKEXWHrgRMptV --- .../java/eu/faircode/netguard/ServiceSinkhole.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java b/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java index 88bbf132..ae1ca369 100644 --- a/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java +++ b/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java @@ -2360,9 +2360,13 @@ public void onReceive(Context context, Intent intent) { Util.logExtras(intent); PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); - // TrackerControl: skip VPN rebuild on idle state changes. - // TC's tracker blocking doesn't change based on Doze mode. - Log.i(TAG, "Device idle=" + pm.isDeviceIdleMode() + " (skipping reload)"); + Log.i(TAG, "device idle=" + pm.isDeviceIdleMode()); + + // Reload rules when coming from idle mode + // This ensures the native tunnel thread is healthy after Doze + // network restrictions are lifted + if (!pm.isDeviceIdleMode()) + reload("idle state changed", ServiceSinkhole.this, false); } };