Files
mesh-catalog/modules/systemd-resolved/README.md
T
jochen d53571213c
mesh/merge-gate fail: builds new: modules/systemd-resolved, sent nowhere; no bus step; a manifest the change touches fails the module check: modules/system…
mesh/repo-check fail: its merge-check.sh failed: long-running resources without health: 70
mesh/delivery superseded: a newer head of the same pull request
systemd-resolved: a machine's own resolver, routing a VPN's domains by link (hq ADR 0247)
Holds node-resolver and provides split-dns at the machine's reach, for a
machine whose VPN client pushes resolvers of its own. It writes the
resolver file naming the machine's private address, gives resolved the
mesh's resolvers as the default route, and serves routes, route and unroute
on the mesh and, over a root-only socket, on the machine. Its guard keeps an
outside write of the file for the module that handles it and puts the
module's file back: at once when taken, after 90 s otherwise, so a write
nothing declared to handle is still raised by the node-engine.
2026-10-07 21:28:11 +02:00

78 lines
5.2 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 go over this socket and never over the bus, so they never leave the machine.
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.