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

79 lines
5.3 KiB
Markdown

# 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.