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)