From 6162732e4fdc00ef083f45cb4835d4a53e7cedf2 Mon Sep 17 00:00:00 2001 From: Maksim Merzhanov Date: Thu, 23 Jul 2026 18:01:54 +0300 Subject: [PATCH] android: route device DNS through the awldns interceptor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The awl core now runs a DNS interceptor on an in-tunnel IP, so .awl names resolve inside the tunnel on Android too. Point the VpnService at it. establishTun: when DNS is enabled, addDnsServer(Anywherelan.dnsServerIP()) so all device DNS reaches the interceptor. An empty string (no free IP, or the core couldn't bring it up) means add nothing. The old addDnsServer(upstream) pin now survives only as a fallback for the DNS-disabled + gateway case, to avoid leaking DNS in full-tunnel mode. AppConfig parses dns.disableDNS (forward-compatible) — the kill switch. --- .../kotlin/com/anywherelan/awl/AppConfig.kt | 17 +++++++++----- .../com/anywherelan/awl/MainActivity.kt | 22 +++++++++++++++---- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/android/app/src/main/kotlin/com/anywherelan/awl/AppConfig.kt b/android/app/src/main/kotlin/com/anywherelan/awl/AppConfig.kt index 22a1280..01b1697 100644 --- a/android/app/src/main/kotlin/com/anywherelan/awl/AppConfig.kt +++ b/android/app/src/main/kotlin/com/anywherelan/awl/AppConfig.kt @@ -20,6 +20,7 @@ data class AppConfig( val dnsJson = root.optJSONObject("dns") val dnsConfig = DnsConfig( + disableDNS = dnsJson?.optBoolean("disableDNS", false) ?: false, upstreamDNSAddress = dnsJson?.optString("upstreamDNSAddress", "") ?: "" ) @@ -68,12 +69,18 @@ data class VPNGatewayConfig( // DnsConfig mirrors DNSConfig in github.com/anywherelan/awl/config. // +// `disableDNS` — DNS is off entirely (also the field kill switch for the +// interceptor): establishTun then does not call addDnsServer with the awl +// DNS IP and the Go side does not install the interceptor. +// // `upstreamDNSAddress` — the public resolver (host:port) the Go side forwards -// non-.awl queries to. On Android there is no in-process awl resolver -// (binding :53 needs root; see the TODO in the Go DNSService), so we instead -// point VpnService at this resolver directly via addDnsServer when full-tunnel -// (gateway client) mode is on, so DNS does not leak around the tunnel. The -// port is stripped — VpnService.addDnsServer takes a bare IP. +// non-.awl queries to. Normally the device talks to the in-tunnel awl DNS +// IP (Anywherelan.dnsServerIP) and the Go resolver does the forwarding; +// this address is used by the host directly only when DNS is disabled but +// full-tunnel (gateway client) mode is on — then VpnService pins it via +// addDnsServer so queries do not leak around the tunnel. The port is +// stripped — VpnService.addDnsServer takes a bare IP. data class DnsConfig( + val disableDNS: Boolean, val upstreamDNSAddress: String, ) diff --git a/android/app/src/main/kotlin/com/anywherelan/awl/MainActivity.kt b/android/app/src/main/kotlin/com/anywherelan/awl/MainActivity.kt index 6a02487..9453813 100644 --- a/android/app/src/main/kotlin/com/anywherelan/awl/MainActivity.kt +++ b/android/app/src/main/kotlin/com/anywherelan/awl/MainActivity.kt @@ -145,11 +145,25 @@ class MainActivity : FlutterActivity() { if (config.vpnGateway.clientEnabled) { builder.addRoute("0.0.0.0", 0) builder.addRoute("::", 0) + } - // Full-tunnel mode: pin DNS to the configured upstream so queries go - // through the TUN (and the exit node) instead of leaking to the - // device's own resolver. The Go DNSService cannot take over DNS on - // Android (no awl :53 listener without root), so the host owns this. + // The Go core owns an in-tunnel DNS IP (netstack interceptor in the + // TUN read path): .awl names are answered locally, everything else is + // forwarded to the configured upstream — in gateway client mode + // through the tunnel, so DNS does not leak. The IP is inside the awl + // subnet that addAddress already routes on-link, so no extra addRoute + // is needed. An empty string means DNS is off or the Go side could + // not set the interceptor up (e.g. no free IP in the subnet). + val dnsServerIP = if (!config.dns.disableDNS) Anywherelan.dnsServerIP() else "" + if (dnsServerIP.isNotEmpty()) { + builder.addDnsServer(dnsServerIP) + // TODO: builder.addSearchDomain("awl") — would resolve bare + // short names (e.g. "mypeer") without the .awl suffix. + } else if (config.vpnGateway.clientEnabled) { + // No interceptor (DNS disabled — the kill switch — or the Go side + // failed to set it up) while full-tunnel mode is on: pin DNS to + // the configured upstream so queries go through the TUN (and the + // exit node) instead of leaking to the device's own resolver. val dnsHost = hostFromAddress(config.dns.upstreamDNSAddress) if (dnsHost.isNotEmpty()) { builder.addDnsServer(dnsHost)