From e9166082d1e0b6da355f80cdbe7f3f66fdc9bfa8 Mon Sep 17 00:00:00 2001 From: Konrad Kollnig <5175206+kasnder@users.noreply.github.com> Date: Fri, 23 Oct 2026 11:48:08 +0200 Subject: [PATCH 1/2] Add a toggle to temporarily use network DNS --- .../faircode/netguard/ActivitySettings.java | 3 + .../eu/faircode/netguard/ServiceSinkhole.java | 7 +- .../missioncontrol/LocalNetworkAccess.java | 7 +- app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/preferences.xml | 7 ++ .../eu/faircode/netguard/CustomDnsTest.java | 110 ++++++++++++++++++ .../LocalNetworkAccessTest.java | 11 ++ 7 files changed, 142 insertions(+), 5 deletions(-) create mode 100644 app/src/test/java/eu/faircode/netguard/CustomDnsTest.java diff --git a/app/src/main/java/eu/faircode/netguard/ActivitySettings.java b/app/src/main/java/eu/faircode/netguard/ActivitySettings.java index d1f71804a..6595a6deb 100644 --- a/app/src/main/java/eu/faircode/netguard/ActivitySettings.java +++ b/app/src/main/java/eu/faircode/netguard/ActivitySettings.java @@ -803,6 +803,9 @@ else if ("vpn4".equals(name)) { getString(R.string.setting_dns, prefs.getString(name, "-"))); ServiceSinkhole.reload("changed " + name, this, false); + } else if ("custom_dns_enabled".equals(name)) { + ServiceSinkhole.reload("changed " + name, this, false); + } else if ("doh_enabled".equals(name)) { // Reset DoH client when toggled net.kollnig.missioncontrol.dns.DnsOverHttpsClient.resetInstance(); diff --git a/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java b/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java index 5869aaaad..9f9568ef8 100644 --- a/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java +++ b/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java @@ -1584,8 +1584,11 @@ public static List getDns(Context context) { // Get custom DNS servers SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); boolean ip6 = prefs.getBoolean("ip6", true); - String vpnDns1 = prefs.getString("dns", null); - String vpnDns2 = prefs.getString("dns2", null); + // Preserve saved addresses when temporarily using the network's DNS. + // Default on so upgrades and older backups retain their DNS behaviour. + boolean customDns = prefs.getBoolean("custom_dns_enabled", true); + String vpnDns1 = customDns ? prefs.getString("dns", null) : null; + String vpnDns2 = customDns ? prefs.getString("dns2", null) : null; Log.i(TAG, "DNS system=" + TextUtils.join(",", sysDns) + " config=" + vpnDns1 + "," + vpnDns2); // First pass: IPv4 resolvers only. This reproduces the historical diff --git a/app/src/main/java/net/kollnig/missioncontrol/LocalNetworkAccess.java b/app/src/main/java/net/kollnig/missioncontrol/LocalNetworkAccess.java index 45ab416a8..921351fc4 100644 --- a/app/src/main/java/net/kollnig/missioncontrol/LocalNetworkAccess.java +++ b/app/src/main/java/net/kollnig/missioncontrol/LocalNetworkAccess.java @@ -94,7 +94,7 @@ private LocalNetworkAccess() { /** Preferences that can point TrackerControl at the local network. */ private static final String[] SETTINGS = { - "dns", "dns2", // custom VPN DNS servers + "dns", "dns2", "custom_dns_enabled", // custom VPN DNS servers "doh_enabled", "doh_endpoint", // Secure DNS "tcp_mss_clamp", // tethering compatibility mode "wg_enabled", "wg_config", // WireGuard remote egress @@ -220,8 +220,9 @@ private static boolean isConfigured(Context context) { } static boolean isConfigured(SharedPreferences prefs) { - if (isLocalAddress(prefs.getString("dns", null)) || - isLocalAddress(prefs.getString("dns2", null))) + if (prefs.getBoolean("custom_dns_enabled", true) && + (isLocalAddress(prefs.getString("dns", null)) || + isLocalAddress(prefs.getString("dns2", null)))) return true; if (prefs.getBoolean("doh_enabled", false) && diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e7aae0a28..806407ee3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -163,6 +163,8 @@ VPN IPv4: %s VPN IPv6: %s VPN DNS: %s + Use custom DNS + Turn off to use your network’s DNS without deleting the saved addresses. Secure DNS and remote VPN DNS take precedence. Validate at: %s Minimum DNS TTL: %s s Use SOCKS5 proxy diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 9115ae0b8..7e47522b3 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -145,10 +145,17 @@ android:hint="fd00:1:fd00:1:fd00:1:fd00:1" android:inputType="text" android:key="vpn6" /> + Date: Fri, 23 Oct 2026 12:02:07 +0200 Subject: [PATCH 2/2] Make WireGuard stop race tests wait for lifecycle invalidation --- .../wg/WgConnectivityMonitorTest.kt | 68 ++++++++++--------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/app/src/test/java/net/kollnig/missioncontrol/wg/WgConnectivityMonitorTest.kt b/app/src/test/java/net/kollnig/missioncontrol/wg/WgConnectivityMonitorTest.kt index 9f6f5f43d..71d244fe5 100644 --- a/app/src/test/java/net/kollnig/missioncontrol/wg/WgConnectivityMonitorTest.kt +++ b/app/src/test/java/net/kollnig/missioncontrol/wg/WgConnectivityMonitorTest.kt @@ -23,7 +23,8 @@ class WgConnectivityMonitorTest { } private fun awaitStopped(monitor: WgConnectivityMonitor) { - repeat(1_000) { + val deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5) + while (System.nanoTime() < deadline) { if (!monitor.isRunning()) return Thread.sleep(1) } @@ -353,25 +354,28 @@ class WgConnectivityMonitorTest { clock = { 0L }, beforeCallback = { callbackReady.countDown() - assertTrue(releaseCallback.await(1, TimeUnit.SECONDS)) + releaseCallback.await() } ) - monitor.start() - assertTrue(callbackReady.await(1, TimeUnit.SECONDS)) - Thread { + val stopper = Thread { monitor!!.stop() stopReturned.countDown() - }.start() - // stop invalidates the generation before waiting for the callback gate. - repeat(1_000) { - if (!monitor.isRunning()) return@repeat - Thread.yield() } - assertFalse(monitor.isRunning()) - releaseCallback.countDown() - assertTrue(stopReturned.await(1, TimeUnit.SECONDS)) - assertEquals(0, callbacks.get()) + monitor.start() + try { + assertTrue(callbackReady.await(5, TimeUnit.SECONDS)) + stopper.start() + // Wait for invalidation, not an arbitrary number of scheduler hints. + awaitStopped(monitor) + releaseCallback.countDown() + assertTrue(stopReturned.await(5, TimeUnit.SECONDS)) + assertEquals(0, callbacks.get()) + } finally { + releaseCallback.countDown() + monitor.stop() + stopper.join(5_000) + } } @Test @@ -400,29 +404,31 @@ class WgConnectivityMonitorTest { clock = { now.get() }, beforeCallback = { prodReady.countDown() - assertTrue(releaseProd.await(1, TimeUnit.SECONDS)) + releaseProd.await() } ) - monitor.start() - assertTrue(prodReady.await(1, TimeUnit.SECONDS)) - Thread { + val stopper = Thread { monitor!!.stop() stopReturned.countDown() - }.start() - // stop invalidates the generation first, then waits for the callback - // gate held by beforeCallback. It cannot return while that gate is - // unresolved. - repeat(1_000) { - if (!monitor.isRunning()) return@repeat - Thread.yield() } - assertFalse(monitor.isRunning()) - assertFalse(stopReturned.await(100, TimeUnit.MILLISECONDS)) - releaseProd.countDown() - assertTrue(stopReturned.await(1, TimeUnit.SECONDS)) - awaitStopped(monitor) - assertEquals(0, prods.get()) + monitor.start() + try { + assertTrue(prodReady.await(5, TimeUnit.SECONDS)) + stopper.start() + // stop invalidates the generation first, then waits for the callback + // gate held by beforeCallback. It cannot return while that gate is + // unresolved. + awaitStopped(monitor) + assertFalse(stopReturned.await(100, TimeUnit.MILLISECONDS)) + releaseProd.countDown() + assertTrue(stopReturned.await(5, TimeUnit.SECONDS)) + assertEquals(0, prods.get()) + } finally { + releaseProd.countDown() + monitor.stop() + stopper.join(5_000) + } } @Test