> Source: https://nexusatlas.dev/docs/concepts/dns — Nexus Atlas developer documentation (Concepts). Converted from the HTML page; the page is canonical.

# Mesh DNS opt-in · default off

Every node already floods [link-state advertisements](https://nexusatlas.dev/docs/concepts/mesh) carrying its name and the tunnel prefixes it serves — which means every node already holds a distributed, self-healing, eventually-consistent name→address database. Mesh DNS is a presentation layer on top of it: each `atlasd` runs a small local resolver bound to its own tunnel address, answering `<node_name>.atlas` straight from its own copy of the LSDB, with split-horizon overrides and per-suffix forwarding for everything else. There is no DNS server anywhere in the mesh — resolution is per-node and local, exactly like the tunnel itself: no controller, no coordination server, no reachback required.

## The LSDB is already a name database

Overlay products that offer "magic" names (Tailscale's MagicDNS is the familiar example) run a local resolver too — but populate it from their coordination server. Atlas populates its resolver from the routing protocol the mesh already runs: the same LSAs that tell SPF where a node's prefixes live also say what the node is called. Same UX; no control plane in the path. The consequences fall out for free:

- **Resolution never leaves the node.** A query for a mesh name is answered from local memory — it does not cross a link, a relay, or an uplink. There is nothing to reach, so there is nothing whose outage can break naming.

- **Partitions keep their names.** Split a mesh in half and each half keeps resolving its own half's names, because each half has its own LSDB. A coordination-server design cannot even learn the map without its server.

- **Names track topology.** A node that joins is resolvable as soon as its LSA floods; a node that leaves stops resolving when its advertisement ages out. Short TTLs (below) keep client caches honest.

> **DNS is a tenant of the tunnel, never a prerequisite for it.** Peer endpoints in the config remain IP addresses, and nothing in tunnel establishment ever performs a DNS lookup. Mesh DNS rides the tunnel once it exists; it can never become the reason the tunnel does not.

## The [dns] table

The whole surface, with defaults:

**/etc/atlas/config.toml**

```
[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"]
```

| Key | Default | Behavior |
|-----|---------|----------|
| enabled | false | Master switch. Off means no socket is opened and nothing anywhere else in the daemon changes — the feature is invisible until you turn it on. |
| listen | [interface] address, port 53 | Addresses the resolver binds — UDP and TCP on each. atlasd config check hard-refuses wildcard binds (0.0.0.0, [::]) as an open-resolver/amplification risk, and warns when an address is neither the tunnel nor loopback. Refusing to start beats starting wrong. |
| domain | "atlas" | The zone suffix. A node's FQDN is <node_name>.<domain> — gcs.atlas, or isr01.narva.atlas when the name itself carries dots. |
| mode | "split" | split: the mesh zone is answered locally, everything else is proxied to upstream. mesh-only: out-of-zone queries get REFUSED immediately and nothing is ever forwarded — the EMCON posture. |
| upstream | Quad9 (9.9.9.9, 149.112.112.112) | Where out-of-zone queries relay in split mode. Point it at your own infrastructure if you have opinions; the default matches the conventions used elsewhere in the daemon. |
| upstream_timeout_ms | 800 | Hard per-query deadline on the upstream relay. Expiry returns SERVFAIL to the client — a dead upstream produces a fast, honest error, never a hang. |
| serve_reverse | true | Serve PTR zones over the tunnel prefixes, so 10.0.100.7 reverse-resolves to its FQDN — the difference between a topology view full of addresses and one full of names. |

## Names, sanitization, conflicts

The name is the `node_name` every node [already advertises in its LSAs](https://nexusatlas.dev/docs/concepts/mesh#topology-extras) (falling back to the hostname), joined to `domain`. Names may contain dots — `node_name = "isr01.narva"` with the default domain yields `isr01.narva.atlas` — and this phase changes nothing on the wire: the resolver is a pure consumer of advertisements the mesh already floods.

Because `node_name` is free text and DNS labels are not, names are sanitized deterministically: lowercased, with every run of characters outside `[a-z0-9-.]` collapsed to a single `-` — `"FOB ALPHA"` becomes `fob-alpha`. Each label is capped at 63 characters and the full sanitized name at 128.

Two nodes claiming one name is a configuration mistake the mesh must survive: resolution is **first-claim-wins**, with a deterministic tiebreak on the lowest NodeId so every node in the mesh gives the same answer, and a loud `WARN` in the [journal](https://nexusatlas.dev/docs/concepts/telemetry) so you actually fix it. Name↔key binding — proving a name belongs to a key, not merely detecting the collision — ships with signed zone bundles in a [later phase](https://nexusatlas.dev/docs/concepts/dns#roadmap).

## What the resolver answers

| Record | Source | Notes |
|--------|--------|-------|
| A | The named node's /32 IPv4 prefixes from its LSA. | The common case — one tunnel address per node. |
| AAAA | The named node's /128 IPv6 prefixes. | Rare in today's IPv4-inner-tunnel deployments, but fully supported. |
| PTR | Reverse zones over the tunnel prefixes (serve_reverse). | Address → FQDN, for topology views, logs and traceroute that read like a map. |
| CNAME | [[dns.override]] entries only. | Mesh nodes themselves are always answered with addresses, never aliases. |
| SOA / NS | Synthetic, at the zone apex. | Enough for resolver stacks that insist on zone furniture; there is no real zone file anywhere. |

Positive answers carry a **30 s TTL**, negative answers **10 s** — the LSDB is live and names must track topology, so nothing is cacheable for long. The resolver distinguishes `NXDOMAIN` (the name does not exist) from `NODATA` (the name exists but not that record type — an `AAAA` query for an IPv4-only node returns an empty answer, not an error). Client stacks cache and fall back differently for the two; conflating them is how `getaddrinfo` stalls end up blamed on the network.

## Split versus mesh-only

In the default `split` mode the resolver answers the mesh zone authoritatively and relays everything else upstream — so you can point a host's resolver at the tunnel address and the whole namespace keeps working. The upstream path is deliberately *not* a recursive resolver:

- Incoming queries are parsed **only** far enough to read QNAME and QTYPE (compression-aware) — enough to decide "ours or not", nothing more.

- A query that is not ours is relayed as the **raw datagram**, on a **fresh ephemeral socket per query** — source-port randomization by construction — with only the transaction ID rewritten. The answer is relayed back raw, unparsed.

- The wire format is hand-rolled and the dependency count is zero: there is no DNS library to inherit a parser CVE from, because there is almost no parsing.

- Every relayed query carries the `upstream_timeout_ms` deadline; expiry returns `SERVFAIL`. Fail closed, fail fast.

`mesh-only` is the EMCON posture: out-of-zone queries are answered `REFUSED` immediately and nothing is forwarded — no query ever leaves the node on an uplink, and a chatty OS full of telemetry-phoning software can be pointed at the resolver without leaking a single lookup off-mesh.

In both modes the listener speaks UDP and TCP on each configured address and refuses queries from off-tunnel sources.

## Overrides and forwards, worked

Two escape hatches cover the names that are not mesh nodes. An **override** is split-horizon DNS for one exact name: queried over the tunnel it gets your authoritative answer; queried anywhere else, public DNS answers as usual.

```
[[dns.override]]
name = "video.acme.com"
a = ["10.0.100.7"]        # aaaa = [...] and cname = "..." also accepted
```

A field client resolving `video.acme.com` through the tunnel now lands on the mesh node actually serving the feed at `10.0.100.7` — while the same laptop on hotel Wi-Fi, resolver pointed elsewhere, gets the public CDN answer. No public zone edit, no split-brain zone files to keep in sync.

A **forward** delegates a whole suffix to specific servers — typically a customer's internal resolver that is only reachable over the tunnel:

```
[[dns.forward]]
suffix = "corp.acme.com"
servers = ["10.0.100.53"]
```

Any name under `corp.acme.com` is relayed to `10.0.100.53` instead of the public upstream — so intranet names resolve for tunnel clients even though no public server has ever heard of them. Overrides match one exact name and answer authoritatively; forwards catch everything under their suffix; the mesh zone is always answered locally; whatever remains follows `mode`.

## The hosts-file escape hatch

Some environments will not let you touch the resolver configuration at all — locked-down images, minimal containers, the odd distro with a resolver stack all its own. For those, the daemon prints the name database instead of serving it:

**any mesh node**

```
$ atlasd dns hosts
10.0.100.1    gcs.atlas
10.0.100.7    isr01.narva.atlas
10.0.100.12   fob-alpha.atlas
```

`/etc/hosts`-format lines for every currently known mesh name — append them to a hosts file, feed them to a template engine, or cron a refresh. Prefer the live resolver when you can (hosts files do not expire when topology changes); prefer this when touching the resolver is the harder fight.

## Operational guidance

- **Bind only the tunnel and loopback.** The default — the `[interface]` address on port 53 — is right for almost everyone. `config check` makes the dangerous spelling impossible and warns about the questionable ones.

- **Pointing the OS at it is a manual step today.** With systemd-resolved, scope the mesh domain to the tunnel interface rather than replacing the global resolver: `resolvectl dns nexus0 10.0.100.1` and `resolvectl domain nexus0 '~atlas'` sends only mesh-zone queries to Atlas and leaves everything else untouched. Watch NetworkManager and resolved on bearer events — both are happy to rewrite resolver state when links flap, which is exactly when an Atlas deployment is busiest. First-class resolved integration is a [roadmap item](https://nexusatlas.dev/docs/concepts/dns#roadmap); until then, treat resolver wiring as part of your deployment automation, next to the [systemd unit](https://nexusatlas.dev/guides/systemd).

- **Android:** the app's engine port does not yet plumb the resolver into `VpnService` DNS — mesh names on Android are future wiring, not a current feature.

- **EMCON deployments:** run `mode = "mesh-only"` and the resolver becomes provably silent off-mesh — the correct default wherever emission control or exfiltration surface matters more than resolving the public internet.

## Security posture, honestly

**Names are not identity.** Names inherit the LSDB trust model: any member of the mesh can advertise any name. What ships now is conflict detection with a deterministic tiebreak and a loud warning — not proof of ownership. Cryptographic name↔key binding arrives with signed zone bundles in a later phase; until then, authentication is what it has always been in Atlas — static public keys — and names are convenience labels on top.

- **It cannot be an open resolver.** The resolver never binds a wildcard address (config validation hard-refuses it) and refuses recursion for off-tunnel sources — there is no configuration in which it participates in a DNS amplification attack.

- **No query ever crosses a relay.** Mesh relaying is [hop-by-hop encrypted](https://nexusatlas.dev/docs/concepts/mesh#crypto-boundary), so a centralized in-mesh resolver would have exposed every lookup to relay operators. Per-node local resolution makes that structurally impossible rather than merely discouraged.

- **No cross-tenant surface.** The LSDB only ever contains the mesh you are enrolled in — there is no shared namespace, no global registry, and nothing to enumerate from outside the tunnel.

## Partition and failure behavior

The design goal, stated plainly: names that survive the loss of the thing that hands out names — because nothing hands out names. Partition a mesh and each side keeps resolving every node it can still route to, from its own LSDB; when the partition heals, the databases converge and so do the answers. A departed node's name disappears when its advertisement ages out, and the 30 s/10 s TTLs keep client caches from lying much longer than that.

Upstream failure is bounded by construction: a dead — or deliberately denied — upstream costs each out-of-zone query exactly `upstream_timeout_ms` and returns `SERVFAIL` — and in-mesh resolution never touches an uplink, so a contested or severed backhaul cannot delay a mesh-name answer by a single millisecond. The failure domains do not overlap, in either direction.

## Where this goes next roadmap

Everything above is the current, shipping surface. Three phases follow — presented as direction, not as features you can configure today:

- **Structure and tooling:** a first-class `site` field in the LSAs (today, dotted `node_name`s carry site structure by convention), built-in systemd-resolved integration replacing the manual `resolvectl` step, and `dns doctor` diagnostics.

- **Signed zone bundles:** console-authored zones — records, overrides, forwards — signed offline and cached on disk at each node, so a control-plane outage degrades nothing; the same trust pattern as the signed relay map in the [traversal stack](https://nexusatlas.dev/docs/concepts/traversal#hosted). This is also where name↔key binding lands, closing the trust caveat above.

- **Public delegation for ACME DNS-01 only** — enough delegation to answer certificate challenges, bringing real TLS to node dashboards. The trade-off will be stated as plainly then as now: publishing node names publicly (or through certificate-transparency logs) leaks force structure, so the default remains what it is today — names resolve only inside the tunnel.
