From 32a78e69914811ef3bb1d317fb83e7ccceb6b070 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 05:08:16 +0000 Subject: [PATCH 1/2] Support Android 17 local network protections (#701) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Android 17 gates traffic to local network addresses behind the ACCESS_LOCAL_NETWORK runtime permission for apps targeting API 37, which TrackerControl does: TCP connections time out and UDP fails with EPERM. Traffic other apps send to the LAN is unaffected — those are their own sockets, and RFC 1918 ranges stay out of the VPN routes, so that traffic never enters the tun. What breaks is traffic TrackerControl itself sends to the local network: a custom VPN DNS server on the LAN gets a host route into the tun and is re-sent from our own socket, so pointing the VPN DNS at a Pi-hole or the router leaves the device with no working name resolution at all. The same applies to Secure DNS aimed at a local resolver, to tethering compatibility mode (whose full-tunnel route puts LAN traffic back inside the tun), and to a WireGuard peer hosted at home. Declare the permission and ask for it where the configuration actually needs it: immediately when such a setting changes, and from a tappable warning on the main screen while it is missing. The system's own resolvers are deliberately left out — Android exempts port 53 traffic to the network's DNS servers, so the common "router is the DNS server" setup keeps working, and users who never point TrackerControl at the LAN are never prompted. Below Android 17 nothing is checked or shown. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01MsAkkinA9Jfjt9khTKvBma --- app/src/main/AndroidManifest.xml | 8 + .../eu/faircode/netguard/ActivityMain.java | 41 +++ .../faircode/netguard/ActivitySettings.java | 17 ++ .../eu/faircode/netguard/ServiceSinkhole.java | 7 + .../missioncontrol/LocalNetworkAccess.java | 265 ++++++++++++++++++ app/src/main/res/layout/main.xml | 13 + app/src/main/res/values/strings.xml | 1 + .../LocalNetworkAccessTest.java | 192 +++++++++++++ 8 files changed, 544 insertions(+) create mode 100644 app/src/main/java/net/kollnig/missioncontrol/LocalNetworkAccess.java create mode 100644 app/src/test/java/net/kollnig/missioncontrol/LocalNetworkAccessTest.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3e9cc5f7..57b779ff 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,6 +13,14 @@ + + 0 && + grantResults[0] == PackageManager.PERMISSION_GRANTED); + TextView tvLocalNetwork = findViewById(R.id.tvLocalNetwork); + if (tvLocalNetwork != null) + tvLocalNetwork.setVisibility(granted ? View.GONE : View.VISIBLE); + if (granted) + // The tunnel keeps its sockets across a reload, but the native + // engine reopens them, so LAN destinations become reachable. + ServiceSinkhole.reload("permission granted", this, false); + else if (!ActivityCompat.shouldShowRequestPermissionRationale(this, + LocalNetworkAccess.PERMISSION)) + // Permanently denied: the prompt no longer appears, so send the + // user to the permission screen instead. + try { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + Uri uri = Uri.fromParts("package", getPackageName(), null); + intent.setData(uri); + startActivity(intent); + } catch (Throwable ex) { + Log.e(TAG, ex + "\n" + ex.getStackTrace()); + } } } diff --git a/app/src/main/java/eu/faircode/netguard/ActivitySettings.java b/app/src/main/java/eu/faircode/netguard/ActivitySettings.java index e436cd15..09e3d65a 100644 --- a/app/src/main/java/eu/faircode/netguard/ActivitySettings.java +++ b/app/src/main/java/eu/faircode/netguard/ActivitySettings.java @@ -74,6 +74,7 @@ import androidx.work.WorkManager; import net.kollnig.missioncontrol.BuildConfig; +import net.kollnig.missioncontrol.LocalNetworkAccess; import net.kollnig.missioncontrol.R; import net.kollnig.missioncontrol.data.BlockingMode; import net.kollnig.missioncontrol.data.InternetBlocklist; @@ -127,6 +128,9 @@ public void run() { private static final int REQUEST_EXPORT = 1; private static final int REQUEST_IMPORT = 2; private static final int REQUEST_CALL = 5; + private static final int REQUEST_LOCAL_NETWORK = 6; + + private boolean requestedLocalNetwork = false; private static final Intent INTENT_VPN_SETTINGS = new Intent("android.net.vpn.SETTINGS"); @@ -966,6 +970,19 @@ else if ("domain_based_blocking".equals(name)) { TrackerList.reloadTrackerData(this); } + // Android 17 blocks traffic to local network addresses unless the user + // grants ACCESS_LOCAL_NETWORK. Ask as soon as a setting starts pointing + // TrackerControl at the LAN — a custom DNS server, a local Secure DNS + // resolver, a WireGuard peer at home, or the full-tunnel tethering + // mode — rather than letting name resolution fail silently (#701). + // 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 (!requestedLocalNetwork && LocalNetworkAccess.isRelevantSetting(name) + && LocalNetworkAccess.isMissing(this)) { + requestedLocalNetwork = true; + requestPermissions(new String[] { LocalNetworkAccess.PERMISSION }, REQUEST_LOCAL_NETWORK); + } } private CharSequence getWireGuardStatusSummary(SharedPreferences prefs) { diff --git a/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java b/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java index d46e6b36..1035bba6 100644 --- a/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java +++ b/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java @@ -1660,6 +1660,13 @@ private Builder getBuilder(List listAllowed, List listRule) { Log.e(TAG, "addRoute DNS " + dns + ": " + ex); } + // 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)) + Log.w(TAG, "Local network access not granted: configured LAN destinations" + + " (custom DNS, Secure DNS resolver, WireGuard peer) are unreachable"); + // 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 // TC excludes itself from the VPN (addDisallowedApplication), so this DNS resolution diff --git a/app/src/main/java/net/kollnig/missioncontrol/LocalNetworkAccess.java b/app/src/main/java/net/kollnig/missioncontrol/LocalNetworkAccess.java new file mode 100644 index 00000000..6d4ebd96 --- /dev/null +++ b/app/src/main/java/net/kollnig/missioncontrol/LocalNetworkAccess.java @@ -0,0 +1,265 @@ +/* + * This file is part of TrackerControl. + * + * TrackerControl is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TrackerControl is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with TrackerControl. If not, see . + */ + +package net.kollnig.missioncontrol; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.os.Build; +import android.text.TextUtils; +import android.util.Log; + +import androidx.core.content.ContextCompat; +import androidx.preference.PreferenceManager; + +import net.kollnig.missioncontrol.wg.WgConfig; +import net.kollnig.missioncontrol.wg.WgConfigParser; +import net.kollnig.missioncontrol.wg.WgPeer; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.URI; + +/** + * Android 17 (API 37) "local network protections": traffic to and from local + * network addresses requires the {@code ACCESS_LOCAL_NETWORK} runtime + * permission for apps targeting API 37 or higher. Without it, TCP connections + * time out and UDP fails with {@code EPERM}. + * + *

Traffic other apps send to the LAN is unaffected by TrackerControl's + * permission state: those are their own sockets, and TrackerControl keeps RFC + * 1918 ranges out of its routes (see {@link eu.faircode.netguard.VpnRoutes}), + * so that traffic never enters the tun. What does depend on this permission is + * traffic TrackerControl itself sends to the local network: + * + *

    + *
  • a custom VPN DNS server on the LAN (Pi-hole, AdGuard Home, the router). + * Such resolvers get a host route into the tun, so the queries are + * re-sent from TrackerControl's own socket (#701);
  • + *
  • Secure DNS (DoH) pointed at a local resolver — an ordinary HTTPS + * connection, with no DNS exemption to fall back on;
  • + *
  • tethering compatibility mode, which installs a full-tunnel default + * route, so LAN traffic no longer bypasses the VPN;
  • + *
  • a WireGuard peer hosted on the LAN, whose endpoint socket is local.
  • + *
+ * + *

The system's own resolvers are deliberately not treated as needing the + * permission: Android exempts port 53 traffic to the network's DNS servers, so + * the common "router is the DNS server" setup keeps working untouched. Only + * configuration that points TrackerControl somewhere else on the LAN triggers + * the prompt, which keeps the permission request off the path of users who + * never need it. + */ +public class LocalNetworkAccess { + private static final String TAG = "TrackerControl.LocalNet"; + + /** + * Runtime permission guarding local network access. Referenced by name + * because {@code Manifest.permission.ACCESS_LOCAL_NETWORK} only exists in + * API 36+ SDKs, and the manifest declares the same string. + */ + public static final String PERMISSION = "android.permission.ACCESS_LOCAL_NETWORK"; + + /** Android 17. Enforcement applies to apps targeting this level or higher. */ + private static final int SDK_LOCAL_NETWORK_PROTECTION = 37; + + /** Preferences that can point TrackerControl at the local network. */ + private static final String[] SETTINGS = { + "dns", "dns2", // custom VPN DNS servers + "doh_enabled", "doh_endpoint", // Secure DNS + "tcp_mss_clamp", // tethering compatibility mode + "wg_enabled", "wg_config", // WireGuard remote egress + }; + + /** Whether the running Android version enforces local network protections. */ + public static boolean isEnforced() { + return Build.VERSION.SDK_INT >= SDK_LOCAL_NETWORK_PROTECTION; + } + + /** Whether changing {@code name} can change {@link #isMissing(Context)}. */ + public static boolean isRelevantSetting(String name) { + for (String setting : SETTINGS) + if (setting.equals(name)) + return true; + return false; + } + + public static boolean isGranted(Context context) { + if (!isEnforced()) + return true; + return ContextCompat.checkSelfPermission(context, PERMISSION) + == PackageManager.PERMISSION_GRANTED; + } + + /** + * Whether local network access is both needed by the current configuration + * and not granted — i.e. whether something the user configured is about to + * break, or has already broken. + */ + 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); + } + + /** Whether the current configuration makes TrackerControl talk to the LAN. */ + public static boolean isConfigured(Context context) { + return isConfigured(PreferenceManager.getDefaultSharedPreferences(context)); + } + + public static boolean isConfigured(SharedPreferences prefs) { + if (isLocalAddress(prefs.getString("dns", null)) || + isLocalAddress(prefs.getString("dns2", null))) + return true; + + if (prefs.getBoolean("doh_enabled", false) && + isLocalUrlHost(prefs.getString("doh_endpoint", null))) + return true; + + if (prefs.getBoolean("tcp_mss_clamp", false)) + return true; + + return prefs.getBoolean("wg_enabled", false) && + hasLocalWireGuardEndpoint(prefs.getString("wg_config", null)); + } + + /** + * Whether {@code address} is a numeric address on the local network: an RFC + * 1918 range, the RFC 6598 range some routers use on their LAN side, a + * link-local address, or an IPv6 unique local address. Only literals are + * considered — resolving a hostname here would mean a network lookup on the + * caller's (often main) thread. + */ + static boolean isLocalAddress(String address) { + if (TextUtils.isEmpty(address)) + return false; + + InetAddress addr = parseNumeric(address.trim()); + if (addr == null) + return false; + + if (addr.isLoopbackAddress() || addr.isAnyLocalAddress()) + return false; // The device itself, not the local network + if (addr.isLinkLocalAddress() || addr.isSiteLocalAddress()) + return true; + if (addr instanceof Inet6Address) { + // Unique local addresses (fc00::/7) — isSiteLocalAddress() only + // covers the deprecated fec0::/10 range. + byte[] bytes = addr.getAddress(); + return (bytes[0] & 0xFE) == 0xFC; + } + return isCarrierGradeNat(addr); + } + + /** + * Parses a numeric IPv4/IPv6 address, returning null for anything else. + * Deliberately does not fall back to {@link InetAddress#getByName(String)} + * for names, which would resolve them over the network. + */ + private static InetAddress parseNumeric(String value) { + try { + if (value.indexOf(':') >= 0) { + // IPv6 literal; drop any zone index (fe80::1%wlan0). + int zone = value.indexOf('%'); + String literal = (zone < 0 ? value : value.substring(0, zone)); + // Colons cannot appear in host names, so this never resolves. + return InetAddress.getByName(literal); + } + + String[] parts = value.split("\\.", -1); + if (parts.length != 4) + return null; + byte[] bytes = new byte[4]; + for (int i = 0; i < 4; i++) { + if (parts[i].isEmpty() || parts[i].length() > 3) + return null; + for (int c = 0; c < parts[i].length(); c++) + if (parts[i].charAt(c) < '0' || parts[i].charAt(c) > '9') + return null; + int octet = Integer.parseInt(parts[i]); + if (octet > 255) + return null; + bytes[i] = (byte) octet; + } + return InetAddress.getByAddress(bytes); + } catch (Throwable ex) { + Log.w(TAG, "Cannot parse address: " + ex); + return null; + } + } + + /** 100.64.0.0/10 (RFC 6598), used by some routers for their LAN side. */ + private static boolean isCarrierGradeNat(InetAddress addr) { + if (!(addr instanceof Inet4Address)) + return false; + byte[] bytes = addr.getAddress(); + return (bytes[0] & 0xFF) == 100 && (bytes[1] & 0xC0) == 0x40; + } + + /** Whether {@code url}'s host is a local network address literal. */ + static boolean isLocalUrlHost(String url) { + if (TextUtils.isEmpty(url)) + return false; + try { + String host = URI.create(url.trim()).getHost(); + if (host == null) + return false; + // URI keeps the brackets around IPv6 literals. + if (host.startsWith("[") && host.endsWith("]")) + host = host.substring(1, host.length() - 1); + return isLocalAddress(host); + } catch (Throwable ex) { + Log.w(TAG, "Cannot parse URL: " + ex); + return false; + } + } + + /** Whether any peer of {@code config} has an endpoint on the local network. */ + static boolean hasLocalWireGuardEndpoint(String config) { + if (TextUtils.isEmpty(config)) + return false; + try { + WgConfig parsed = WgConfigParser.INSTANCE.parse(config); + for (WgPeer peer : parsed.getPeers()) { + String endpoint = peer.getEndpoint(); + if (endpoint == null) + continue; + if (isLocalAddress(hostOfEndpoint(endpoint))) + return true; + } + } catch (Throwable ex) { + Log.w(TAG, "Cannot parse WireGuard config: " + ex); + } + return false; + } + + /** Strips the port from a WireGuard {@code host:port} endpoint. */ + private static String hostOfEndpoint(String endpoint) { + String value = endpoint.trim(); + if (value.startsWith("[")) { // [fd00::1]:51820 + int end = value.indexOf(']'); + return end < 0 ? value : value.substring(1, end); + } + int colon = value.lastIndexOf(':'); + // A bare IPv6 literal has several colons and no port. + if (colon < 0 || value.indexOf(':') != colon) + return value; + return value.substring(0, colon); + } +} diff --git a/app/src/main/res/layout/main.xml b/app/src/main/res/layout/main.xml index 19656fa9..32249ecc 100644 --- a/app/src/main/res/layout/main.xml +++ b/app/src/main/res/layout/main.xml @@ -73,6 +73,19 @@ android:textColor="?attr/colorOff" android:visibility="visible" /> + + Detect trackers in app code 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. Android 17 blocks it by default, so your own DNS server, Secure DNS resolver or WireGuard peer cannot be reached. 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. diff --git a/app/src/test/java/net/kollnig/missioncontrol/LocalNetworkAccessTest.java b/app/src/test/java/net/kollnig/missioncontrol/LocalNetworkAccessTest.java new file mode 100644 index 00000000..3760bdd2 --- /dev/null +++ b/app/src/test/java/net/kollnig/missioncontrol/LocalNetworkAccessTest.java @@ -0,0 +1,192 @@ +/* + * This file is part of TrackerControl. + * + * TrackerControl is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TrackerControl is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with TrackerControl. If not, see . + */ + +package net.kollnig.missioncontrol; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.content.SharedPreferences; + +import androidx.preference.PreferenceManager; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +/** + * Which configurations make TrackerControl talk to the local network, and thus + * need the Android 17 {@code ACCESS_LOCAL_NETWORK} permission (#701). The SDK + * gating itself is not covered: Robolectric runs on API 36, below the level + * where local network protections are enforced. + */ +@RunWith(RobolectricTestRunner.class) +public class LocalNetworkAccessTest { + private static final String KEY = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; + private static final String WG_CONFIG_TEMPLATE = + "[Interface]\n" + + "PrivateKey = " + KEY + "\n" + + "Address = 10.64.0.2/32\n" + + "\n" + + "[Peer]\n" + + "PublicKey = " + KEY + "\n" + + "AllowedIPs = 0.0.0.0/0\n" + + "Endpoint = %s\n"; + + private SharedPreferences prefs; + + @Before + public void setUp() { + prefs = PreferenceManager.getDefaultSharedPreferences(RuntimeEnvironment.getApplication()); + prefs.edit().clear().commit(); + } + + @Test + public void defaultConfigurationDoesNotNeedLocalNetworkAccess() { + assertFalse(LocalNetworkAccess.isConfigured(prefs)); + } + + @Test + public void publicCustomDnsDoesNotNeedLocalNetworkAccess() { + prefs.edit().putString("dns", "9.9.9.9").commit(); + assertFalse(LocalNetworkAccess.isConfigured(prefs)); + } + + @Test + public void privateCustomDnsNeedsLocalNetworkAccess() { + prefs.edit().putString("dns", "192.168.1.10").commit(); + assertTrue(LocalNetworkAccess.isConfigured(prefs)); + } + + @Test + public void privateSecondaryDnsNeedsLocalNetworkAccess() { + prefs.edit().putString("dns", "9.9.9.9").putString("dns2", " 10.0.0.53 ").commit(); + assertTrue(LocalNetworkAccess.isConfigured(prefs)); + } + + @Test + public void uniqueLocalIpv6DnsNeedsLocalNetworkAccess() { + prefs.edit().putString("dns", "fd12:3456:789a::1").commit(); + assertTrue(LocalNetworkAccess.isConfigured(prefs)); + } + + @Test + public void loopbackDnsDoesNotNeedLocalNetworkAccess() { + // The device itself is not the local network. + prefs.edit().putString("dns", "127.0.0.1").commit(); + assertFalse(LocalNetworkAccess.isConfigured(prefs)); + } + + @Test + public void localDohEndpointNeedsLocalNetworkAccessOnlyWhenEnabled() { + prefs.edit().putString("doh_endpoint", "https://192.168.1.10/dns-query").commit(); + assertFalse(LocalNetworkAccess.isConfigured(prefs)); + + prefs.edit().putBoolean("doh_enabled", true).commit(); + assertTrue(LocalNetworkAccess.isConfigured(prefs)); + } + + @Test + public void bracketedIpv6DohEndpointNeedsLocalNetworkAccess() { + prefs.edit() + .putBoolean("doh_enabled", true) + .putString("doh_endpoint", "https://[fd00::1]:8443/dns-query") + .commit(); + assertTrue(LocalNetworkAccess.isConfigured(prefs)); + } + + @Test + public void publicDohEndpointDoesNotNeedLocalNetworkAccess() { + prefs.edit() + .putBoolean("doh_enabled", true) + .putString("doh_endpoint", "https://dns.quad9.net/dns-query") + .commit(); + assertFalse(LocalNetworkAccess.isConfigured(prefs)); + } + + @Test + public void tetheringCompatibilityModeNeedsLocalNetworkAccess() { + // Full-tunnel routes put LAN traffic back inside the tun. + prefs.edit().putBoolean("tcp_mss_clamp", true).commit(); + assertTrue(LocalNetworkAccess.isConfigured(prefs)); + } + + @Test + public void remoteWireGuardEndpointDoesNotNeedLocalNetworkAccess() { + prefs.edit() + .putBoolean("wg_enabled", true) + .putString("wg_config", String.format(WG_CONFIG_TEMPLATE, "185.65.135.72:51820")) + .commit(); + assertFalse(LocalNetworkAccess.isConfigured(prefs)); + } + + @Test + public void selfHostedWireGuardEndpointNeedsLocalNetworkAccessOnlyWhenEnabled() { + prefs.edit() + .putString("wg_config", String.format(WG_CONFIG_TEMPLATE, "192.168.1.5:51820")) + .commit(); + assertFalse(LocalNetworkAccess.isConfigured(prefs)); + + prefs.edit().putBoolean("wg_enabled", true).commit(); + assertTrue(LocalNetworkAccess.isConfigured(prefs)); + } + + @Test + public void ipv6WireGuardEndpointNeedsLocalNetworkAccess() { + prefs.edit() + .putBoolean("wg_enabled", true) + .putString("wg_config", String.format(WG_CONFIG_TEMPLATE, "[fd00::5]:51820")) + .commit(); + assertTrue(LocalNetworkAccess.isConfigured(prefs)); + } + + @Test + public void hostnameEndpointsAreNotTreatedAsLocal() { + // Hostnames are never resolved here — that would be a network lookup on + // the caller's thread. + prefs.edit() + .putBoolean("wg_enabled", true) + .putString("wg_config", String.format(WG_CONFIG_TEMPLATE, "vpn.example.org:51820")) + .commit(); + assertFalse(LocalNetworkAccess.isConfigured(prefs)); + } + + @Test + public void malformedValuesAreIgnored() { + prefs.edit() + .putString("dns", "not-an-address") + .putString("dns2", "999.1.1.1") + .putBoolean("doh_enabled", true) + .putString("doh_endpoint", "not a url") + .putBoolean("wg_enabled", true) + .putString("wg_config", "garbage") + .commit(); + assertFalse(LocalNetworkAccess.isConfigured(prefs)); + } + + @Test + public void relevantSettingsAreRecognised() { + assertTrue(LocalNetworkAccess.isRelevantSetting("dns")); + assertTrue(LocalNetworkAccess.isRelevantSetting("doh_endpoint")); + assertTrue(LocalNetworkAccess.isRelevantSetting("tcp_mss_clamp")); + assertTrue(LocalNetworkAccess.isRelevantSetting("wg_config")); + assertFalse(LocalNetworkAccess.isRelevantSetting("blocking_mode")); + assertFalse(LocalNetworkAccess.isRelevantSetting(null)); + } +} From 9c17dc5a70f76a5d09ae698246b03d19b188147f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 05:08:22 +0000 Subject: [PATCH 2/2] Update whois lookup link (NetGuard upstream) Merges NetGuard 552ef140: dnslytics moved its lookup from /whois-lookup/ to search.dnslytics.com/ip/. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01MsAkkinA9Jfjt9khTKvBma --- app/src/main/java/eu/faircode/netguard/ActivityLog.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/eu/faircode/netguard/ActivityLog.java b/app/src/main/java/eu/faircode/netguard/ActivityLog.java index 79c816ac..2c87d6e1 100644 --- a/app/src/main/java/eu/faircode/netguard/ActivityLog.java +++ b/app/src/main/java/eu/faircode/netguard/ActivityLog.java @@ -198,7 +198,7 @@ protected void onCreate(Bundle savedInstanceState) { popup.getMenu().findItem(R.id.menu_protocol).setTitle(Util.getProtocolName(protocol, version, false)); // Whois - final Intent lookupIP = new Intent(Intent.ACTION_VIEW, Uri.parse("https://www.dnslytics.com/whois-lookup/" + ip)); + final Intent lookupIP = new Intent(Intent.ACTION_VIEW, Uri.parse("https://search.dnslytics.com/ip/" + ip)); if (pm.resolveActivity(lookupIP, 0) == null) popup.getMenu().removeItem(R.id.menu_whois); else