Documentation
¶
Overview ¶
Package androiddns resolves DNS names on Android by speaking the dnsproxyd protocol to the system DNS resolver daemon over its unix socket, the same mechanism bionic libc's getaddrinfo uses.
It exists for pure Go (non-cgo) binaries built for Android, such as CLI tools run under Termux. Android has no /etc/resolv.conf, so Go's built-in resolver has no nameservers and every lookup fails. Binaries built with cgo don't have this problem: Go's net package forces the cgo resolver on Android (golang/go#10714), which calls bionic's getaddrinfo, which consults the same daemon this package talks to directly. Queries through dnsproxyd get the system's per-network DNS configuration and Private DNS (DNS over TLS/HTTPS) handling for free.
Protocol compatibility ¶
The wire protocol is unofficial but effectively frozen, for a structural reason rather than a policy one. Its two halves live on opposite sides of Android's update split. The client side (bionic's getaddrinfo proxy and libnetd_client) ships in the OS system image and updates only with a full OS update, which for most devices means rarely and eventually never. The server side has been the DnsResolver mainline (APEX) module since Android 10, updated via Google Play across all supported OS releases at once. A single current module binary must therefore keep serving the frozen libc clients of every supported Android version simultaneously, so existing commands cannot change semantics, much like a kernel syscall ABI with the update roles inverted. On the wire this package is indistinguishable from an Android 10 era bionic client, which cannot be broken without breaking DNS on real devices.
The daemon, its socket, and the getaddrinfo-level commands date to 2010 (Android Gingerbread); see system/netd commit 007e987fee and bionic commit a1dbf0b453. The raw-packet resnsend command used here was added in November 2018 (system/netd commit c0c818f448) to back the android_res_nsend NDK API introduced in Android 10 (API 29). For the current protocol definition, see ResNSendCommand in packages/modules/DnsResolver/DnsProxyListener.cpp and resNetworkSend in system/netd/client/NetdClient.cpp.
Android 9 and older have no resnsend; their daemon answers it with a textual "500 Command not recognized". On the first such reply this package switches (for the rest of the process) to the older getaddrinfo command and synthesizes a DNS answer from the addrinfo list the daemon returns. That path can only answer A and AAAA questions; other query types fail with an error. It works on Android 6.0 (API 23) through 9: the command's arguments have been the same since Android 5.0 added netId, but 5.x sent the reply in an older layout that this package rejects with an error rather than parse. The fallback matters in practice: Amazon's Fire OS 7 devices run Android 9 and were still getting new firmware on it in 2026 (tailscale/tailcat#126).
Connecting to the socket requires membership in the AID_INET group, which app UIDs (including Termux) hold via the INTERNET permission, and SELinux policy grants app domains connect (but not stat) access to the socket, since every app's libc performs this exact connect. The plausible residual risk is not protocol change but a future policy tightening that distinguishes callers, so users of this package should treat it as best effort; the automatic installation into net.DefaultResolver (see auto.go) probes the socket with a connect first and leaves the resolver alone on failure.
Scope and gating ¶
The package also builds on GOOS=linux, not just GOOS=android, because static linux binaries run fine under Android kernels (Termux users and rooted devices commonly run our official static linux/arm64 builds) and have the same broken resolver there. Those builds detect Android at runtime before installing anything: no /etc/resolv.conf, the presence of /dev/__properties__ (the bionic property service's backing store, present since Android 5.0 and never on regular Linux), and a successful dnsproxyd connect.
This is an optional feature, included by default in tailscaled builds on Linux and Android; build with the ts_omit_androiddns tag to omit it. It is not linked into tsnet by default; tsnet apps and other programs opt in with a blank import of this package.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func NewResolver ¶
NewResolver returns a net.Resolver that resolves names via the system resolver daemon instead of Go's usual name resolution paths.
func Query ¶
Query sends the wire-format DNS query msg to the system resolver daemon and returns the wire-format answer. The answer's ID matches the query's ID. An unsuccessful rcode (such as NXDOMAIN) is not an error; it's returned in the answer's header for the caller to interpret.
The query is resolved on the default network with the system's usual policy for the calling UID, as if the process had called bionic's getaddrinfo.
On Android 10 and later the query goes to the daemon as is and the answer is whatever the upstream server returned. On Android 9 and older the daemon can only do getaddrinfo-style lookups, so only A and AAAA questions can be answered; see the package documentation.
Types ¶
This section is empty.