From 019685b668b6eae62c9e5e6d43798b2f10e8a92f Mon Sep 17 00:00:00 2001 From: Konrad Kollnig <5175206+kasnder@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:52:50 +0200 Subject: [PATCH] Notify about missing local network access, and stop misdiagnosing it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The banner only reaches someone who opens the app, and the settings prompt only someone already changing that setting. The symptom is "nothing resolves", which nobody attributes to a permission — the likely responses are to turn the VPN off or uninstall, neither of which the app ever hears about. Add a notification from where the tunnel is built, so the reason meets the user where the failure appears. It alerts once, clears as soon as the permission is granted, and opens the main screen, where the warning row requests it. The existing notifications actively misdiagnose this case. A LAN resolver we may not talk to raises the DoH one — "Secure DNS cannot be reached ... consider disabling it in the Network settings" — which blames the wrong thing and recommends turning off a feature that works fine; a WireGuard peer on the LAN sends the user into the WireGuard settings for the same non-reason. Both now check isMissing() first and defer to the local network notification, which names the actual cause and fixes it in one tap. Co-Authored-By: Claude Opus 5 --- .../eu/faircode/netguard/ServiceSinkhole.java | 63 ++++++++++++++++++- app/src/main/res/values/strings.xml | 2 + 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java b/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java index 1035bba6..e1811301 100644 --- a/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java +++ b/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java @@ -230,6 +230,7 @@ public class ServiceSinkhole extends VpnService { public static final int NOTIFY_EXTERNAL = 9; private static final int NOTIFY_DOH_ERROR = 11; private static final int NOTIFY_WG_ERROR = 12; + private static final int NOTIFY_LOCAL_NETWORK = 13; public static final String EXTRA_COMMAND = "Command"; private static final String EXTRA_REASON = "Reason"; @@ -1663,9 +1664,15 @@ private Builder getBuilder(List listAllowed, List listRule) { // Android 17 refuses local network traffic without ACCESS_LOCAL_NETWORK: // TCP times out, UDP fails with EPERM. A LAN resolver routed into the tun // above is re-sent from our own socket, so it goes silent (#701). - if (net.kollnig.missioncontrol.LocalNetworkAccess.isMissing(ServiceSinkhole.this)) + // The symptom is "nothing resolves", which no one attributes to a + // permission — and the banner only reaches someone who opens the app. + // Notify, so the reason meets the user where the failure appears. + if (net.kollnig.missioncontrol.LocalNetworkAccess.isMissing(ServiceSinkhole.this)) { Log.w(TAG, "Local network access not granted: configured LAN destinations" + - " (custom DNS, Secure DNS resolver, WireGuard peer) are unreachable"); + " (custom DNS, Secure DNS resolver, WireGuard peer, proxy) are unreachable"); + showLocalNetworkNotification(); + } else + clearLocalNetworkNotification(); // Dynamically exclude carrier ePDG IPs so Wi-Fi calling works globally. // ePDG domains follow 3GPP standard: epdg.epc.mnc{MNC}.mcc{MCC}.pub.3gppnetwork.org @@ -3662,6 +3669,14 @@ private void showErrorNotification(String message) { } private void showDohErrorNotification() { + // A local resolver we are not allowed to reach is not a DoH problem, + // and "consider disabling Secure DNS" is the wrong advice for it: the + // fix is one permission away. Say so instead (#701). + if (net.kollnig.missioncontrol.LocalNetworkAccess.isMissing(this)) { + showLocalNetworkNotification(); + return; + } + Intent main = new Intent(this, ActivityMain.class); PendingIntent pi = PendingIntentCompat.getActivity(this, 0, main, PendingIntent.FLAG_UPDATE_CURRENT); @@ -3686,6 +3701,14 @@ private void showDohErrorNotification() { } private void showWireGuardErrorNotification(String message) { + // Same as the DoH case: a peer on the LAN that we may not talk to is a + // permission problem, not a tunnel problem, and sending the user to the + // WireGuard settings only wastes their time (#701). + if (net.kollnig.missioncontrol.LocalNetworkAccess.isMissing(this)) { + showLocalNetworkNotification(); + return; + } + Intent main = new Intent(this, ActivitySettings.class); PendingIntent pi = PendingIntentCompat.getActivity(this, NOTIFY_WG_ERROR, main, PendingIntent.FLAG_UPDATE_CURRENT); @@ -3717,6 +3740,42 @@ private void clearWireGuardErrorNotification() { NotificationManagerCompat.from(this).cancel(NOTIFY_WG_ERROR); } + /** + * Local network access is missing and something the user configured needs + * it. Opens the main screen, where the warning row requests the permission. + */ + private void showLocalNetworkNotification() { + Intent main = new Intent(this, ActivityMain.class); + PendingIntent pi = PendingIntentCompat.getActivity(this, NOTIFY_LOCAL_NETWORK, main, + PendingIntent.FLAG_UPDATE_CURRENT); + + String detail = getString(R.string.msg_local_network_notify); + + NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "notify"); + builder.setSmallIcon(R.drawable.ic_error_white_24dp) + .setContentTitle(getString(R.string.msg_local_network_title)) + .setContentText(detail) + .setContentIntent(pi) + .setColor(getResources().getColor(R.color.colorTrackerControl)) + .setOngoing(false) + .setAutoCancel(true) + // Rebuilt on every network change; alert once, then sit quietly. + .setOnlyAlertOnce(true); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + builder.setCategory(NotificationCompat.CATEGORY_STATUS) + .setVisibility(NotificationCompat.VISIBILITY_SECRET); + + NotificationCompat.BigTextStyle notification = new NotificationCompat.BigTextStyle(builder); + notification.bigText(detail); + + Util.notify(this, NOTIFY_LOCAL_NETWORK, notification.build()); + } + + private void clearLocalNetworkNotification() { + NotificationManagerCompat.from(this).cancel(NOTIFY_LOCAL_NETWORK); + } + private void showUpdateNotification(String name, String url) { if (Util.isFDroidInstall()) return; diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2f2f9a81..3dc0023a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -694,6 +694,8 @@ Sincerely,\n\n]]> Loading cached tracker results failed. Please report this to the developer at hello@trackercontrol.org Tap to grant notification permissions (for error messages, etc.) Tap to allow local network access, which Android asks about as \"nearby devices\". Android 17 blocks it by default, so your own DNS server, Secure DNS resolver, WireGuard peer or proxy cannot be reached. + Local network access needed + TrackerControl cannot reach your own DNS server, Secure DNS resolver, WireGuard peer or proxy, so name resolution may fail. Android 17 requires permission for this, which Android asks about as \"nearby devices\". Tap to open TrackerControl and allow it. Monitor system apps Route system apps (Play Services, carrier services, etc.) through the VPN so their trackers are detected and blocked, and show them in the app list.\n\nOff by default: excluding system apps is friendlier to battery, because their background traffic no longer wakes the VPN. Turning this on conflicts with \"Block connections without VPN\" in the Android VPN settings, which must be DISABLED.