Mesh DNS

Names that survive the loss of the thing that hands out names.

Every atlasd already floods, mesh-wide, an advertisement carrying its node_name and the prefixes it serves — a distributed, self-healing name→address database that every node holds a full copy of. Mesh DNS is a presentation layer on top of it: a small resolver on each node, bound to its own tunnel address, answering for the mesh straight from its own LSDB. There is no DNS server anywhere in the mesh — no controller, no coordination server, no reachback required.

Opt-in ([dns], default off)No DNS server anywhere in the mesh
Already flooding

node_name and prefixes ride the LSA

Every link-state advertisement the mesh already floods carries the originator’s human node_name and the exact prefixes it serves. Nothing new crosses the wire for Mesh DNS — no protocol change, no extra flood — the data was there all along, keeping itself current as topology moved.

Already replicated

Every node holds the whole database

The LSDB is a full copy per node, eventually consistent and self-healing — the properties a distributed name database is supposed to be hard to get. The dashboard’s topology view has always been a rendering of it; the namespace is simply a second rendering of the same source of truth.

Presentation layer

DNS as a rendering of the LSDB

Enable [dns] and the node answers A, AAAA and PTR for mesh names from its own database, with split-horizon overrides and per-suffix forwarding for everything else. When a node joins, moves or dies, its name follows within an advertisement tick — which is why the zone’s TTLs are deliberately short. The database is live; the names must track it.

The shape

Resolution is local, like the tunnel itself.

The same design rule that removed the controller removes the DNS server: each node answers from what it already knows. One flood feeds one database feeds one resolver — on every node, independently.

EVERY NODE FLOODS · EVERY NODE HOLDS THE MAP · EVERY NODE ANSWERS isr01.narvaLSA: name + 10.0.100.7/32 fob-alphaLSA: name + 10.0.100.12/32 gs-northLSA: name + 10.0.100.1/32 LSA FLOOD Per-node LSDBthe full name→address map, on every node RENDERED AS DNS Local resolverbound to the tunnel address · 10.0.100.1:53 MESH ZONE SPLIT MODE isr01.narva.atlasanswered from the LSDB, locally example.orgforwarded raw · fresh socket · 800 ms deadline no server, no uplink — a jammed uplink cannot block this answer mesh-only mode: answered REFUSED — nothing leaves on an uplink
MESH ZONEanswered locally, straight from this node’s own LSDB
EVERYTHING ELSEproxied raw upstream in split mode — REFUSED in mesh-only
Per node

A resolver on every node, not a server for the fleet

Each configured address gets a UDP and a TCP listener — by default the node’s own tunnel address, port 53 — and off-tunnel sources are refused. Clients ask the node they are sitting on; the answer comes out of memory. Nothing is elected, nothing is designated, and there is no resolver whose loss the fleet would notice.

No relay in the path

No query ever crosses a relay

Mesh relay crypto is hop-by-hop, so a centralized in-mesh resolver would have exposed every lookup to relay operators. Per-node local resolution makes that structurally impossible — the query never leaves the node that asked it. And there is no cross-tenant surface to reason about: an LSDB only ever contains the mesh you’re enrolled in.

Tenant, not prerequisite

DNS rides the tunnel; the tunnel never needs DNS

Peer endpoints remain IP addresses, and nothing in tunnel establishment ever performs a DNS lookup. That ordering is deliberate: the daemon’s no-DNS-dependency property holds exactly as before, and name resolution becomes one more service the tunnel carries rather than something it waits for.

Partition

Each half keeps resolving its own half

Split the mesh and both sides keep answering for every name they can still see, because both sides hold their own database. When the partition heals, the LSDBs reconcile like everything else. There is no half that goes dark waiting for a name server that lives on the other side — no such server exists.

Names

node_name, sanitized — dots welcome

The name is the LSA’s node_name (hostname fallback), and it may contain dots: isr01.narva under domain atlas resolves as isr01.narva.atlas — site structure without any protocol change. Names are sanitized to DNS-safe labels: lowercase, runs of characters outside [a-z0-9-.] collapse to a hyphen, so “FOB ALPHA” becomes fob-alpha. Labels cap at 63 characters, the full sanitized name at 128.

Forward

A and AAAA from the prefixes already advertised

A records come from the named node’s /32 IPv4 prefixes in its advertisement; AAAA from its /128 IPv6 prefixes — rare in today’s deployments, and served correctly anyway. The address a name resolves to is the address the routing layer would carry you to, by construction.

Reverse

PTR over the tunnel prefixes

With serve_reverse = true, reverse zones are synthesized for our tunnel prefixes — so a traceroute, an audit log or a monitoring screen prints isr01.narva.atlas where it used to print a bare tunnel address.

Apex

Synthetic SOA and NS; NXDOMAIN vs NODATA, correctly

The zone apex answers with synthetic SOA and NS records, and negative answers distinguish a name that doesn’t exist from a name with no record of the asked type — the details resolvers and debugging tools quietly depend on. Positive answers carry a 30 s TTL, negatives 10 s: the LSDB is live, and names must track topology rather than outlive it.

Conflicts

First claim wins, deterministically

Two nodes claiming one name resolve identically everywhere: first-claim-wins with a tiebreak on the lowest node ID, plus a loud WARN in the log — a name conflict is a configuration error to surface, not a condition to paper over. Cryptographic ownership of a name — name↔key binding — is a later phase, the phase ladder ↗

Two modes

Split for garrison. Mesh-only for EMCON.

mode = "split" answers the mesh zone locally and proxies everything else to a configured upstream. mode = "mesh-only" answers everything else REFUSED, immediately — nothing is forwarded, and no query ever leaves on an uplink.

split

The mesh zone is answered from the LSDB; every other query relays to the configured upstream — Quad9 by default, matching the daemon’s existing convention — under a hard deadline. One resolver address serves both worlds, which is what lets a node drop into an existing namespace without renaming anything.

mesh-only

The EMCON posture: out-of-zone queries are answered REFUSED on the spot. Nothing is forwarded, nothing is queued for later, and no lookup ever leaves on a public uplink — radio discipline applied to name resolution, enforced by the resolver rather than by procedure.

Failure is a fast answer

A dead upstream produces SERVFAIL at the 800 ms deadline — never a hang, never a retry storm. And because the mesh zone never touches the upstream path at all, a jammed uplink cannot block in-mesh resolution: the half of the namespace that matters in the field keeps answering regardless.

The proxy, precisely

QNAME only

Parsed just far enough to route

The resolver parses an incoming query only far enough to read its QNAME and QTYPE — compression-aware, nothing more. A query that isn’t ours is relayed upstream as the raw datagram it arrived as, with only the transaction ID rewritten.

Fresh socket

One ephemeral socket per query

Every forwarded query leaves on a fresh ephemeral socket — source-port randomization by construction — and the raw answer is relayed straight back. No response parsing, no answer rewriting, no cache to poison.

Zero dependencies

Hand-rolled wire format

The DNS wire handling is written in the daemon, not imported: no DNS library joins the dependency tree, and the parsing surface exposed to untrusted input stays exactly as large as the two fields the proxy needs.

[[dns.override]]

One name, two truths

An override answers authoritatively for an exact name when it is queried over the tunnel: video.acme.com resolves to the tunnel address of the node that serves it inside, while public DNS keeps answering for everyone outside. a, aaaa and cname records are all supported — the split-horizon pattern, per name, in three lines.

[[dns.forward]]

Suffix routing to internal resolvers

Per-suffix forwarding sends everything under corp.acme.com to the customer’s internal resolver — a server reachable only over the tunnel. Remote nodes resolve the corporate namespace exactly as a desk inside the building would, and every other query still follows the node’s own mode.

Escape hatch

atlasd dns hosts

One command prints /etc/hosts-format lines for every currently known mesh name — for hosts-file workflows, containers, and the distro where port 53 already belongs to somebody else. The resolver is the product; the hosts file is the guarantee that no environment is locked out of the names.

config.toml — the [dns] block
[dns]
enabled = false                    # default off
listen = ["10.0.100.1:53"]         # default: [interface].address port 53 when enabled.
                                   # config check HARD-REFUSES 0.0.0.0/[::] (open-resolver
                                   # / amplification risk); warns if not tunnel or loopback.
domain = "atlas"                   # zone suffix; FQDN = <node_name>.<domain>
mode = "split"                     # "split" (mesh zone local, rest proxied upstream)
                                   # | "mesh-only" (EMCON: out-of-zone → REFUSED, nothing
                                   #   ever leaves on an uplink)
upstream = ["9.9.9.9", "149.112.112.112"]   # default Quad9 (matches existing convention)
upstream_timeout_ms = 800          # hard deadline; expiry → SERVFAIL, never a hang
serve_reverse = true               # PTR zones for our tunnel prefixes

[[dns.override]]                   # split-horizon: authoritative answer for this exact
name = "video.acme.com"            # name when queried over the tunnel; outside, public
a = ["10.0.100.7"]                 # DNS answers. aaaa = [...] and cname = "..." too.

[[dns.forward]]                    # per-suffix forwarding (customer's internal resolver,
suffix = "corp.acme.com"           # reachable only over the tunnel)
servers = ["10.0.100.53"]
the escape hatch
# every currently known mesh name, in /etc/hosts format
atlasd dns hosts
10.0.100.1    gs-north.atlas
10.0.100.7    isr01.narva.atlas
10.0.100.12   fob-alpha.atlas

The listener never binds a wildcard address — atlasd config check refuses 0.0.0.0 and [::] outright, and warns when a listen address is neither tunnel nor loopback.

Security posture

The trust model, out loud.

A name service inherits the trust model of whatever feeds it. Here that is the LSDB — so here is exactly what that means, in both directions.

Designed out

  • Never an open resolver: the listener never binds a wildcard address, and recursion is refused for off-tunnel sources — the amplification pattern is structurally unavailable, and config validation enforces it before the daemon will run.
  • No query ever crosses a relay: resolution is per-node and local, so relay operators see no lookups — a centralized in-mesh resolver would have handed them every one.
  • No cross-tenant surface: an LSDB only ever contains the mesh you’re enrolled in. There is no shared namespace to leak across estates.
  • Mesh-only mode is an EMCON posture: no query ever leaves on a public uplink, enforced by the resolver rather than by discipline.

Stated plainly

  • Names inherit the LSDB trust model: any enrolled mesh member can advertise a name. Conflict detection with a deterministic tiebreak ships now; cryptographic name↔key binding via signed zone bundles is a later phase, labeled below.
  • In split mode, upstream answers relay back raw — Mesh DNS is not a validating or filtering resolver for the wider internet, and does not claim to be.
  • By default, mesh names resolve only inside the tunnel. Making any of them publicly visible is a later, deliberately narrow phase — with its trade-off printed next to it.
Coordination-fed

Tailscale MagicDNS — and the partition question

Tailscale runs a local resolver too — 100.100.100.100 in tailscaled, and the UX Atlas deliberately matches — but it is populated by their coordination server. Atlas’s resolver is populated by the routing protocol the mesh already runs: same experience at the prompt, no control plane in the path. Under partition, each half of an Atlas mesh keeps resolving its own half’s names, because each half has its own LSDB; a fresh tailnet cannot even learn the map without the coordination server. NetBird follows the same coordination-fed shape.

A server after all

ZeroTier’s zeronsd, Nebula’s lighthouses

ZeroTier resolves overlay names through zeronsd — a sidecar DNS server fed by the controller’s member list: a service to deploy, reach and keep alive. Nebula serves DNS from lighthouse nodes — designated servers inside the overlay that queries must travel to. Atlas has no in-mesh DNS server at all, so there is none to run, none to reach, and none to lose.

Static files

innernet’s hosts file, WireGuard’s pushed resolver

innernet writes overlay names into /etc/hosts — simple and robust, but a snapshot that goes stale between writes. Atlas prints the same file on demand (atlasd dns hosts) while the resolver stays live against the LSDB. Plain WireGuard pushes a resolver address into resolv.conf and leaves the names themselves to whatever that server happens to know.

Where the coordination-fed designs are stronger: a central namespace authority buys real things — SSO-backed identity on names, one console that is authoritative by definition, one place to audit. Atlas reaches for the equivalents through later phases — console-authored, offline-signed zone bundles rather than a live control plane — and until those ship, the honest summary is: their namespace has an owner; ours has a protocol. the full comparison ↗
Phases

Shipped now. Signed later. Public last — narrowly.

The ladder, labeled like the roadmap labels everything: what you can have today versus what is committed but not here yet.

Shipped · opt-in

Everything on this page

The per-node resolver, both modes, forward and reverse zones, overrides, per-suffix forwarding, conflict tiebreak, the hosts escape hatch — [dns] in the current daemon, default off, byte-identical data path when disabled.

Roadmap

Operational integration

A first-class site field in the LSA — today, dotted node_names carry the site structure — plus systemd-resolved integration and a dns doctor diagnostic in the CLI.

Roadmap

Signed zone bundles, and names bound to keys

Console-authored zones shipped as offline-signed, disk-cached bundles — a control-plane outage degrades nothing, the same trust pattern as the signed relay map — and cryptographic binding of a name to a node’s static key, closing the advertise-any-name gap stated in the posture above.

Roadmap

Public delegation for ACME only

Real TLS on node dashboards via DNS-01 — public delegation scoped to certificate issuance and nothing else. The trade-off, printed rather than discovered: publishing node names publicly, or through CT logs, leaks force structure. The default stays what it is today — names that resolve only inside the tunnel.

Request an evaluation

Cut the uplink and ask again.

Point a client at its node, resolve isr01.narva.atlas, then take the uplink away — the answer does not change. An evaluation starts with a conversation with the engineers who built this.