diff --git a/app/src/main/java/eu/faircode/netguard/ActivitySettings.java b/app/src/main/java/eu/faircode/netguard/ActivitySettings.java index 09e3d65a2..e2b3fbc2f 100644 --- a/app/src/main/java/eu/faircode/netguard/ActivitySettings.java +++ b/app/src/main/java/eu/faircode/netguard/ActivitySettings.java @@ -978,6 +978,11 @@ else if ("domain_based_blocking".equals(name)) { // Asked at most once per visit: writing back a trimmed value re-enters // this listener, and a second request while the dialog is up is dropped // by the framework. + if (LocalNetworkAccess.isRelevantSetting(name)) + // The new value may point somewhere else entirely; what we saw the + // old one reach says nothing about it. + LocalNetworkAccess.forgetObservations(); + if (!requestedLocalNetwork && LocalNetworkAccess.isRelevantSetting(name) && LocalNetworkAccess.isMissing(this)) { requestedLocalNetwork = true; diff --git a/app/src/main/java/net/kollnig/missioncontrol/LocalNetworkAccess.java b/app/src/main/java/net/kollnig/missioncontrol/LocalNetworkAccess.java index b7602e99b..c23104581 100644 --- a/app/src/main/java/net/kollnig/missioncontrol/LocalNetworkAccess.java +++ b/app/src/main/java/net/kollnig/missioncontrol/LocalNetworkAccess.java @@ -121,7 +121,56 @@ public static boolean isGranted(Context context) { public static boolean isMissing(Context context) { // Cheapest checks first: nothing to do below Android 17, and parsing the // WireGuard config is pointless once the permission is granted. - return isEnforced() && !isGranted(context) && isConfigured(context); + return isEnforced() && !isGranted(context) + && (observedLocalDestination || isConfigured(context)); + } + + /** + * A destination we resolved to a local network address, seen while running. + * Covers what {@link #isConfigured(SharedPreferences)} structurally cannot: + * a configuration that names a host rather than an address — + * {@code https://pi.hole/dns-query}, a WireGuard endpoint on a dynamic DNS + * name — where classifying it up front would mean resolving a name on + * whichever thread asked, including the main one. + * + *
Not persisted. A stale observation survives no longer than the + * process, and anything still pointing at the LAN re-reports itself as soon + * as it is used again. + */ + private static volatile boolean observedLocalDestination; + + /** + * Report an address TrackerControl is about to talk to. Callers pass what + * they already resolved, so this never performs a lookup itself and is safe + * to call from any thread. + * + *
Only the destination matters, not whether the connection succeeded: if
+ * it is local and the permission is missing, that traffic is blocked.
+ */
+ public static void reportDestination(String address) {
+ // Deliberately not gated on isEnforced(): isMissing() applies that gate
+ // anyway, and keeping this branch out makes the latch testable under
+ // Robolectric, which runs below the enforcement level.
+ if (observedLocalDestination)
+ return;
+ if (isLocalAddress(address)) {
+ Log.i(TAG, "Local network destination observed at runtime");
+ observedLocalDestination = true;
+ }
+ }
+
+ /** Whether a local destination has been seen since the last reset. */
+ static boolean hasObservedLocalDestination() {
+ return observedLocalDestination;
+ }
+
+ /**
+ * Drop what we observed, so a configuration that no longer points at the
+ * LAN stops warning. Called when a relevant setting changes; anything still
+ * local reports itself again on next use.
+ */
+ public static void forgetObservations() {
+ observedLocalDestination = false;
}
/** Whether the current configuration makes TrackerControl talk to the LAN. */
diff --git a/app/src/main/java/net/kollnig/missioncontrol/dns/DnsOverHttpsClient.java b/app/src/main/java/net/kollnig/missioncontrol/dns/DnsOverHttpsClient.java
index ac5759896..47879ba23 100644
--- a/app/src/main/java/net/kollnig/missioncontrol/dns/DnsOverHttpsClient.java
+++ b/app/src/main/java/net/kollnig/missioncontrol/dns/DnsOverHttpsClient.java
@@ -19,6 +19,7 @@
import androidx.preference.PreferenceManager;
import net.kollnig.missioncontrol.BuildConfig;
+import net.kollnig.missioncontrol.LocalNetworkAccess;
import org.xbill.DNS.Message;
import org.xbill.DNS.Record;
@@ -27,6 +28,7 @@
import java.io.File;
import java.io.IOException;
+import java.net.InetAddress;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
@@ -36,6 +38,7 @@
import okhttp3.Cache;
import okhttp3.ConnectionPool;
+import okhttp3.Dns;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
@@ -80,6 +83,17 @@ private DnsOverHttpsClient(Context context, String endpoint) {
.connectionPool(new ConnectionPool(2, 30, TimeUnit.SECONDS))
.cache(getResponseCache(context))
.retryOnConnectionFailure(true)
+ // An endpoint given as a host name can still be a resolver on
+ // the user's own network, which Android 17 blocks us from
+ // reaching without ACCESS_LOCAL_NETWORK. OkHttp resolves it
+ // anyway, so note the address it got rather than looking it up
+ // a second time (#701).
+ .dns(hostname -> {
+ List