DNS resolver
Name resolution is a standalone service, dns-resolver, reached
over bounded synchronous IPC. Its backend is native Rust and depends only on Motor OS
networking and filesystem APIs. It runs with the None role.
Request path
Rust application (std::net::ToSocketAddrs, TcpStream::connect("host:port"), ...)
-> rt.vdso lookup_host
-> moto-dns: bounded synchronous IPC
-> dns-resolver service
-> connected UDP, with TCP fallback
-> sys-io
Numeric addresses and localhost are answered in the runtime without
contacting the service. Other names use a fresh client connection per lookup, so a dead
resolver connection is never cached and a restarted service is found by the next call.
The service keeps four fixed workers and a versioned, pointer-free request and response
format defined in the moto-dns crate (names up to 253 bytes, up to 16
addresses per answer). Because each lookup takes one IPC connection into four workers,
more than 32 concurrent lookups are refused with NotConnected; raising this
is on the follow-up list.
Configuration
The service reads /system/cfg/libc/hosts first and
/system/cfg/libc/resolv.conf second. sys-io owns resolv.conf:
before starting services it publishes the DNS servers from the active static device
configuration atomically, and it republishes the aggregate whenever a DHCP lease changes.
Static servers are declared with dns_servers in sys-net.toml;
DHCP-configured devices use the servers from their leases. At most the first two unique
nameservers are queried, which keeps one-family lookup latency inside the IPC deadline
when both are unavailable.
Transport and validation
Each UDP attempt:
- creates a fresh socket, and with it a freshly randomized ephemeral source port;
- connects it to the selected nameserver, so packets from other sources are discarded by the socket;
- generates a fresh random 16-bit transaction ID;
- sends one recursive A or AAAA question;
- applies a one-second receive deadline;
- validates the transaction ID, the response bit, the opcode, the single echoed question, the canonical question name, type, and class, the response code, record bounds, and compression pointers;
- accepts address records only for the question name or a previously validated CNAME target;
- removes duplicate addresses.
UDP delivery is unreliable, so the backend performs one bounded retransmission, on a new socket with a new transaction ID. NXDOMAIN is final and is not retried. A response with the truncated bit set is repeated over a connected DNS-over-TCP stream using the same query and the same strict parser.
There is no cache. Correct caching requires honoring each record's TTL and a negative-cache policy; an arbitrary service-wide TTL would trade a small speedup for stale answers.
Result contract
The resolver reports one of: success, not found, temporary failure, timeout, out of
memory, unsupported family, system failure, resolver failure, invalid request, or busy.
The runtime maps these to stable Motor error codes, which surface through
std::io::Error. Combined lookups collect IPv4 before IPv6 and then apply the
destination-ordering policy: usable addresses first, with an unusable global IPv6 range
ranked last so that a VM without IPv6 connectivity does not wait on it.
Testing
dns-resolver --self-test covers numeric and hosts-file lookups, malformed
requests, result ordering, restart behavior, live external resolution, and a deterministic
loopback DNS fixture that drops the first UDP query, answers the retransmission with a
truncated response, and completes the lookup over TCP. The regular test suites never go
out to the Internet; only this explicitly separate live check does.