Skip to content

Support Android 17 local network protections (#701) - #704

Draft
kasnder wants to merge 2 commits into
masterfrom
claude/trackercontrol-issue-701-9x335i
Draft

Support Android 17 local network protections (#701)#704
kasnder wants to merge 2 commits into
masterfrom
claude/trackercontrol-issue-701-9x335i

Conversation

@kasnder

@kasnder kasnder commented Aug 4, 2026

Copy link
Copy Markdown
Member

Fixes #701.

The problem

Android 17 makes local network protections mandatory for apps targeting API 37 — which TrackerControl does (targetSdk 37). Traffic to local network addresses now needs the ACCESS_LOCAL_NETWORK runtime permission (NEARBY_DEVICES group); without it TCP connections time out and UDP fails with EPERM.

Traffic other apps send to the LAN is unaffected by our permission state: those are their own sockets, and VpnRoutes keeps RFC 1918 ranges out of the tun, so that traffic never reaches us. What breaks is traffic TrackerControl itself sends to the local network:

  • A custom VPN DNS server on the LAN (the reported case). getConfig() deliberately adds a /32 host route for site-local resolvers so they stay inside the tunnel and pass through the filtering path — which means the query is re-sent from TrackerControl's own socket. With the permission missing that socket is blocked, and the device ends up with no working name resolution at all.
  • Secure DNS pointed at a local resolver — an ordinary HTTPS connection on port 443, with no DNS exemption to fall back on.
  • Tethering compatibility mode — its full-tunnel 0.0.0.0/0 route puts LAN traffic back inside the tun.
  • A WireGuard peer hosted on the LAN — the tunnel's endpoint socket is local.

The change

  • Declare android.permission.ACCESS_LOCAL_NETWORK in the manifest.
  • New net.kollnig.missioncontrol.LocalNetworkAccess: is enforcement active (API ≥ 37), is the permission granted, and does the current configuration actually point TrackerControl at the LAN (the four cases above, address literals only — resolving a hostname here would mean a lookup on the main thread).
  • Request the permission where it is needed:
    • In settings, as soon as a relevant preference changes (dns, dns2, doh_enabled, doh_endpoint, tcp_mss_clamp, wg_enabled, wg_config), so the user grants it at the moment they configure the thing that needs it. Asked at most once per visit — writing back a trimmed value re-enters the listener, and a second request while the dialog is up is dropped by the framework.
    • On the main screen, a tappable warning row (same pattern as the existing notification-permission row) while the permission is missing and the configuration needs it. Permanent denial opens the app's permission screen, as the notification row already does.
  • ServiceSinkhole logs a warning when the tunnel comes up in that state, so the failure is visible in a bug report.

Deliberately not treated as needing the permission: the system's own resolvers. Android exempts port 53 traffic to the network's DNS servers, so the very common "router is the DNS server" setup keeps working, and users who never point TrackerControl at the LAN are never prompted. Below Android 17, isEnforced() short-circuits — no checks, no banner, no config parsing.

This is a breakage-recovery fix rather than a new knob: no preference is added, and the permission is only ever requested for a configuration the user already chose.

NetGuard upstream

Checked NetGuard (M66B/NetGuard) for the same fix and for anything else worth merging since our last sync:

  • Local network protections: not implemented upstream. NetGuard's manifest has no ACCESS_LOCAL_NETWORK and it still targets SDK 36 (Updated to SDK 36, 31 Jul 2026), so it gets the implicit legacy grant and the enforcement never bites. Nothing to merge — the work here is ours.
  • Merged: Updated whois link (552ef140) — dnslytics moved its lookup to search.dnslytics.com/ip/<ip>; our ActivityLog still used the old /whois-lookup/ URL (second commit).
  • Already in TrackerControl: Added AP state receiver (761450e1), Buffered settings output (8a60a7b3).
  • Not applicable: Allow dynamic tethering network interface (b1d598ce) adds dynamic ap_br_wlan* exclusions to NetGuard's route list; our VpnRoutes already excludes all of RFC 1918 by default, and tethering downstreams are handled by the tethering compatibility mode added in Route the full tunnel in tethering compatibility mode (#699) #700. The SDK 36 / insets / action-bar-height work (0a95c6ea, f2ccd1df, 7b152b49, ce67398e and the accompanying refactors) is NetGuard's route to edge-to-edge on API 36; we already target 37 with our own per-activity insets handling, so adopting it would be churn against a codebase that has moved past it.

Testing

LocalNetworkAccessTest (Robolectric) covers the configuration detection: public vs. private custom DNS, IPv6 ULA and loopback, DoH endpoint gated on doh_enabled including a bracketed IPv6 literal, tethering compatibility mode, local vs. remote vs. hostname WireGuard endpoints, and malformed input. The SDK gate itself is not covered — Robolectric runs on API 36, below the enforcement level.

Not verified on a device: this environment has no Android SDK, so the build and the Robolectric run are left to CI. The permission prompt and banner flow on an Android 17 device have not been exercised, and I would like the reporter to confirm that granting the permission restores their LAN DNS setup before this leaves draft.


Generated by Claude Code

claude added 2 commits August 4, 2026 05:08
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MsAkkinA9Jfjt9khTKvBma
Merges NetGuard 552ef140: dnslytics moved its lookup from
/whois-lookup/<ip> to search.dnslytics.com/ip/<ip>.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MsAkkinA9Jfjt9khTKvBma
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for Android 17 Local Network Protections

2 participants