Status: accepted; hosts file implemented, DNS server planned (M6)
Overlay addresses are derived from device keys, so they are stable but
unmemorable — fd3b:ffe9:f81:6f18:41e:c574:c529:5bbf. Reaching a machine by
name is the difference between a working mesh and a pleasant one.
The daemon already holds the full roster, with each peer's name and address, so the information exists. The question is only how to expose it to the resolver.
Two layers.
Now: shrooms hosts writes /etc/hosts entries in a marked block.
Zero dependencies, works immediately, useful for a fixed set of machines.
Next (M6): a DNS server in the daemon, authoritative for the mesh domain only, with per-OS resolver wiring.
/etc/hosts needs root.Every target platform has a supported hook for domain-scoped resolution, and the ordering is the opposite of intuition:
| Mechanism | |
|---|---|
| Android | VpnService.Builder.addDnsServer() + addSearchDomain() — first-class API, declared while building the tunnel |
| macOS | /etc/resolver/<domain> containing nameserver <overlay-ip> |
| Linux | resolvectl dns <iface> <ip> + resolvectl domain <iface> ~<domain> under systemd-resolved |
| Windows | an NRPT rule for the domain suffix |
Android is the easiest of the four, which matters because it is the hardest platform in every other respect. No root, no file editing, no resolver daemon to negotiate with.
Correction, found by shipping it. Android is the easiest place to install
a resolver and the hardest place to scope one. VpnService has no split-DNS:
addDnsServer makes the tunnel's resolver receive every query the device
makes, and addSearchDomain does not narrow that. A resolver that answers only
for the mesh therefore removes the device's name resolution entirely — no
browsing, no app updates — which is what happened on the first build that
enabled it.
So on Android the resolver must forward what is not its own, and the forwarding socket must be protected or it routes into the tunnel it is resolving for. The queries are proxied verbatim and never logged; it is a pipe, not an observation point. The "no forwarding" rule below still holds everywhere the resolver is reached only for its own domain, which is every other platform.
The decisive property is that it is live: the roster is already in memory, so a peer becomes resolvable the moment it announces, with nothing to regenerate.
It is the obvious "names on a local network" answer and every OS supports it, but it is multicast, and a WireGuard mesh is a set of point-to-point tunnels with no multicast. Making it work would mean building multicast forwarding first — more work than the DNS server, and worse.
Authoritative for the mesh domain only, and refusing everything else where the platform allows it to be scoped. A VPN that quietly becomes the system resolver is a surprise nobody wants and a privacy leak besides. Android cannot be scoped, so there it forwards instead — see the correction above. Names outside the suffix are REFUSED rather than NXDOMAIN: REFUSED says "not mine", NXDOMAIN would assert the name exists nowhere, which we cannot know.
Bind port 53 on the overlay address only, so it cannot clash with a resolver
on localhost. That needs CAP_NET_BIND_SERVICE alongside the existing
CAP_NET_ADMIN. A high port plus per-OS port configuration is possible —
macOS's /etc/resolver supports it — but systemd-resolved does not handle it
cleanly, so the capability is the simpler trade.
Domain choice. .internal is formally reserved by ICANN for private use and
is the correct answer; .mesh reads better and is what the hosts command
defaults to. Configurable either way, and worth settling before anyone depends
on it.