Files
mesh-catalog/modules/systemd-resolved/README.md
T
jochen 60641176ab
mesh/merge-gate pass: builds new: modules/systemd-resolved, sent nowhere; no bus step; every machine composes with the change as it did without (4 of 4 com…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery held for a person: merged, and the controller opened no walk for it within 10m0s — nothing it holds follows that branch, or the merge was…
systemd-resolved: say when a route's domains do cross the bus
2026-10-07 21:35:49 +02:00

5.3 KiB

systemd-resolved

A machine's own resolver (novox/hq ADR 0247): systemd-resolved, holding the node seat node-resolver and providing split-dns at the machine's reach. It is assigned only where something on the machine requires split-dns — today a VPN client whose company domains must go to the VPN's own servers while every other name still goes to the mesh's two resolvers. Every other machine has none, and lists the mesh's resolvers in its resolver file as ADR 0223 says.

It knows nothing about any VPN. It routes a set of domains to a set of servers over one link, lists what is routed, and takes a route away. A module wrapping a VPN client that writes /etc/resolv.conf itself carries its own adapter, which calls these verbs. A VPN that tells systemd-resolved its link's DNS itself (NetworkManager's VPNs, WireGuard set up by networkd, Tailscale) needs none.

What it writes

file what
/etc/resolv.conf this machine's own private address alone, with ADR 0223's options. The uplink's holder steps back from this file where this module is assigned (the controller's rule, ADR 0247), so it has one writer
/etc/node-resolver/resolv.conf the same file, kept beside it for the guard to compare with and put back
/etc/node-resolver/suffix the mesh's own domain, which a route may never take
/etc/systemd/resolved.conf.d/50-mesh.conf every mesh resolver as the default route for names (DNS=, Domains=~.), no public fallback, the stub on loopback and on the private address, no cache, no LLMNR or mDNS

On the private address, so containers reach it. A container copies its machine's resolver file and cannot reach the machine's loopback, so the file names the address the machine has on the private network, where resolved also listens (DNSStubListenerExtra). The packet filter admits a machine's own guests and nobody else, so no other machine can ask it. The port is declared from: machine and fixed: a fixed port is a claim on the machine, given to one module, and the mesh's own resolver (dnsmasq, on the anchor and the home server) claims the same one. The two are not meant to share a machine.

No cache. Nothing on the machine keeps a copy of a mesh name or of a "no such name". Each question is asked again, as it was before this resolver.

Its package is systemd's. resolved ships with systemd, whose package belongs to the systemd module holding node-service-manager (ADR 0207). This module declares the service, running and enabled, restarted when its drop-in changes, and nothing to install.

The guard

One long-running process, as root: resolver-tools guard.

  • It keeps the resolver file the module's own. Twice a second it compares /etc/resolv.conf with the kept copy. Another program's write is displaced: the guard keeps what it wrote in /run/node-resolver/displaced/, readable by root alone and gone at the next boot, and names its writer from the file's own header.
  • A write a module takes is put back at once. A module on the machine that reads the write and routes what it needed says it took it, and the module's own file stands again within a second.
  • A write nobody takes stands for 90 seconds, then is put back. 90 seconds is longer than the node-engine needs to see the rewrite twice, so ADR 0241's machine.<m>.systemd-resolved.rewritten is still raised, naming the writer, for a write nothing on the machine declared to handle.
  • Only the mesh's resolvers answer every name. Every five seconds, a link that a network manager gave servers of its own is told it is not a default route for names. Its own domains stay its own.
  • What it says elsewhere — the routes verb's outside_writes, from /run/node-resolver/history.json — is when, the writer's name and what became of each write. It never includes a server or a domain.

Its verbs

On the mesh, through the node's runtime as the operator account (writes escalate with sudo -n):

verb does
<node>/node-resolver.routes (r) the mesh's resolvers, each link given servers of its own with its routed domains and whether it is a default route, and the resolver file's outside writes
<node>/node-resolver.route (a) {link, domains, servers}: these domains, and every name under them, to these servers over this link, and only them. The mesh's own domain and the root are refused
<node>/node-resolver.unroute (a) {link}: that link's domains go to the mesh's resolvers again

On the machine, to its own root processes, over /run/node-resolver/verbs.sock (mode 0600). The path is the seat's, so a caller does not need to know which holder answers. The protocol is one JSON line {"verb": …, "args": {…}} and one JSON line back, {"result": …} or {"error": "…"}. The verbs are the same three, plus:

  • displaced: the write standing now, with what it held, or null;
  • route with takes (the write's id) and by (the module's name): route, then take that write, so the module's file is put back.

A VPN's servers and domains are handed over on this socket, never over the bus. They cross the bus only as the answer to routes when the operator asks it, and nothing keeps that answer.

resolved holds the routes itself, per link, and forgets a link's route when the link goes. Nothing here keeps a table of its own that could disagree with it.