From 53e0023c92114e9fd45454da8a5e5b349a83f2f3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 1 Aug 2026 22:25:49 +0000 Subject: [PATCH] Route the full tunnel in tethering compatibility mode (#699) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before the static VPN route set landed, the tun carried a plain 0.0.0.0/0 route, so tethered traffic — including the downstream subnets and the DHCP broadcast — went through TrackerControl. On devices whose own tethering path is broken, that is what made tethering work at all. The split-tunnel route set removed the default route and, with it, the subnet/tethering/lan preferences, leaving affected users with no way back other than downgrading. Reuse the existing (opt-in, default off) tethering compatibility mode: alongside the TCP MSS clamp it now installs a single 0.0.0.0/0 route, restoring the previous behaviour for the users who need it while the split-tunnel route set stays the default for everyone else. WireGuard keeps precedence — a full tunnel would hand LAN and reserved ranges to the peer, which drops them — so an active profile's AllowedIPs still decide the routes. On Android 13+ the carrier ePDG excludeRoute() calls still apply on top of the default route, so Wi-Fi calling is unaffected. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01FhD7ETjNc5qb5hSw9taYtS --- .../eu/faircode/netguard/ServiceSinkhole.java | 18 +++++++++-- .../java/eu/faircode/netguard/VpnRoutes.java | 21 ++++++++++++ app/src/main/res/values/strings.xml | 2 +- .../eu/faircode/netguard/VpnRoutesTest.java | 32 +++++++++++++++++++ 4 files changed, 69 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java b/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java index c760b859..d46e6b36 100644 --- a/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java +++ b/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java @@ -1623,9 +1623,21 @@ private Builder getBuilder(List listAllowed, List listRule) { // WireGuard AllowedIPs can opt RFC 1918 ranges into the VPN routes; // without WireGuard, private and reserved ranges remain excluded. - List routes = (wgEnabled && !wgAllowedIps.isEmpty()) - ? VpnRoutes.getRoutes(wgAllowedIps) - : VpnRoutes.getRoutes(); + // Tethering compatibility mode instead installs a plain default route, + // which some devices need before they forward tethered traffic into the + // tun at all (#699). WireGuard keeps precedence: a full tunnel would + // hand LAN and reserved traffic to the peer, which drops it. + boolean tetheringCompat = prefs.getBoolean("tcp_mss_clamp", false); + List routes; + if (wgEnabled && !wgAllowedIps.isEmpty()) { + if (tetheringCompat) + Log.i(TAG, "Tethering compatibility routes skipped: WireGuard AllowedIPs apply"); + routes = VpnRoutes.getRoutes(wgAllowedIps); + } else if (tetheringCompat) { + Log.i(TAG, "Using tethering compatibility routes (full tunnel)"); + routes = VpnRoutes.getTetheringRoutes(); + } else + routes = VpnRoutes.getRoutes(); for (IPUtil.CIDR route : routes) try { builder.addRoute(route.address, route.prefix); diff --git a/app/src/main/java/eu/faircode/netguard/VpnRoutes.java b/app/src/main/java/eu/faircode/netguard/VpnRoutes.java index bdaa06d8..ffedccbd 100644 --- a/app/src/main/java/eu/faircode/netguard/VpnRoutes.java +++ b/app/src/main/java/eu/faircode/netguard/VpnRoutes.java @@ -91,6 +91,27 @@ public static List getRoutes() { return cachedRoutes; } + /** + * Returns the full-tunnel route list (a single 0.0.0.0/0 default route) + * used by tethering compatibility mode. + * + *

Some devices only forward tethered traffic into the tun while the VPN + * carries a real default route; with the split-tunnel route set above they + * fall back to the device's own tethering path, which on such devices is + * broken (#699). This also puts the tethering downstream subnets + * (192.168.42/43/44/49) and the 255.255.255.255 DHCP broadcast back inside + * the tunnel, matching the behaviour TrackerControl had before the static + * route set was introduced. + * + *

The cost is that LAN traffic and reserved ranges no longer bypass the + * tunnel, which is why this is opt-in. Carrier ePDG addresses are still + * excluded on Android 13+, where {@code excludeRoute()} applies on top of + * this route. + */ + public static List getTetheringRoutes() { + return Collections.singletonList(new IPUtil.CIDR("0.0.0.0", 0)); + } + /** * Returns the route list for an active WireGuard remote-egress tunnel, * making the profile's AllowedIPs authoritative over the RFC 1918 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ddd218e5..db870527 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -120,7 +120,7 @@ Disable on call Reload on every connectivity change Tethering compatibility mode - Limit TCP segment size to avoid stalled connections on some tethered networks. May reduce performance. + Route all traffic through TrackerControl and limit TCP segment size, to fix tethered connections that stall or get no internet access. Local network access bypasses TrackerControl no longer, and performance may be reduced. Advanced options (for experts) Show system apps diff --git a/app/src/test/java/eu/faircode/netguard/VpnRoutesTest.java b/app/src/test/java/eu/faircode/netguard/VpnRoutesTest.java index 621693f3..1b418ae6 100644 --- a/app/src/test/java/eu/faircode/netguard/VpnRoutesTest.java +++ b/app/src/test/java/eu/faircode/netguard/VpnRoutesTest.java @@ -17,6 +17,7 @@ package eu.faircode.netguard; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -87,6 +88,37 @@ public void emptyOrIpv6OnlyAllowedIpsFallsBackToDefault() throws Exception { assertFalse(isRouted(viaV6, "192.168.1.10")); } + // --- tethering compatibility mode --------------------------------------- + + @Test + public void tetheringRoutesAreASingleDefaultRoute() throws Exception { + List routes = VpnRoutes.getTetheringRoutes(); + + assertEquals(1, routes.size()); + assertEquals(0, routes.get(0).prefix); + + // Everything is inside the tunnel, including the ranges the default + // route set excludes: the tethering downstream subnets and the DHCP + // broadcast the tethered client uses to get a lease (#699). + assertTrue(isRouted(routes, "8.8.8.8")); + assertTrue(isRouted(routes, "192.168.42.129")); // USB tethering + assertTrue(isRouted(routes, "192.168.43.1")); // Wi-Fi tethering + assertTrue(isRouted(routes, "192.168.44.1")); // Bluetooth tethering + assertTrue(isRouted(routes, "10.0.0.1")); + assertTrue(isRouted(routes, "100.64.0.1")); + assertTrue(isRouted(routes, "0.0.0.0")); + assertTrue(isRouted(routes, "255.255.255.255")); // DHCP broadcast + } + + @Test + public void tetheringRoutesDoNotAffectTheDefaultRouteSet() throws Exception { + VpnRoutes.getTetheringRoutes(); + List routes = VpnRoutes.getRoutes(); + + assertFalse(isRouted(routes, "192.168.42.129")); + assertTrue(isRouted(routes, "8.8.8.8")); + } + // --- WireGuard active: AllowedIPs authoritative -------------------------- @Test