Design 50: the own resolver's file waits longer than its pass over a silent mesh resolver (ADR 0678, issue 1748) #587

Merged
mesh-tools merged 2 commits from design/50-resolver-wait into main 2026-11-12 17:51:41 +00:00
3 changed files with 84 additions and 4 deletions
@@ -0,0 +1,59 @@
---
layer: to-be
design: 50
milestone: resolver-wait
updated: 2026-11-12
code: [mesh-catalog modules/systemd-resolved, mesh-host internal/compose/resolution]
decisions:
- "ADR 0678"
- "ADR 0247"
- "ADR 0223"
---
# Design 50, milestone resolver-wait — the own resolver's file waits longer than its pass
**What it builds.** On a machine where a module holds the node seat `node-resolver`, the file `/etc/resolv.conf`
gives its programs a wait longer than the own resolver's pass over one silent mesh resolver to the next, so a lookup
behind a silent first mesh resolver answers late instead of failing (ADR 0678 §1). The value is the main file's,
[§2](../50-split-dns-on-a-machine-with-a-vpn-client.md#2-the-file-and-the-resolver):
`options timeout:3 attempts:2 edns0`. This part holds every line the milestone builds, cited as "design 50 resolver-wait §n". It is built under
issue 1748.
**Where.** Two repositories: mesh-catalog (the module `systemd-resolved`, holder of `node-resolver`: §1) and `mesh-host`
(the node-engine's names check and its test: §3). One milestone, its two pull requests one delivery group with one
check. No ordering edge joins them: once §3's test reads the options from the catalog's template, each repository's
check passes against the other's main, whichever merges first.
## Lines
1. **The own resolver's template carries the longer options.** The module `systemd-resolved` renders the file twice
from one template, its facts `resolvers` (the live file) and `kept` (the kept copy beside resolved's
configuration, which the guard puts back); both carry the options line of design 50 §2, `options timeout:3
attempts:2 edns0`, in place of `options timeout:1 attempts:2 edns0`, which goes from the template. The one
`nameserver` line, the machine's private address, is unchanged. The node-engine writes both when it renders the
module, the kept copy first (design 50, *As built*), so the guard never compares a new live file with an old copy.
Read by `<node>/node-reads.read` of the file and by the uplink seat's `resolvers` verb, as today. Checked by the
module's manifest test, which asserts the one server and this options line in both facts; the guard's own test,
which holds a literal of the file, carries the new line.
2. **Nothing else of the resolver changes** (ADR 0678 §2). resolved's drop-in stays as it is: no cache, the mesh's
resolvers as its servers in the order of the module's roster (ADR 0223 §9), `~.`, no fallback, its extra listener
on the private address. The uplink modules' files (`networkmanager`, `systemd-networkd`) keep both mesh resolvers
with `timeout:1 attempts:2`, held by the existing test over every uplink's resolver file. The captured copies of
the catalog in other repositories' test data are left as they are: no check reads them.
3. **The names check judges the file with the wait its programs give it.** The node-engine's names check already
bounds each ask by the `timeout:` of the file it judges, never under 1 s; that stays, so on a machine holding
`node-resolver` each ask's bound becomes 3 s. Its test that composes the catalog's own resolver on a machine
reads the expected options line from the catalog's `systemd-resolved` template beside it, in place of the literal
`timeout:1 attempts:2 edns0` it asserts today (ADR 0678, *How it is checked*). The reason: a test literal of
another repository's template fails one repository's check on whichever merges first, the kind issue 1748's note
of 2026-11-10 met.
4. **The value stays 3 s per try until the pass can be read.** design 50 §2's `timeout:3 attempts:2` stays ADR
0678's starting value until a read of resolved's own server switches exists. A restart of resolved and lookups
at the own resolver with its first mesh resolver silent answered in 44 to 46 ms (the lead's reading of
2026-11-12 18:05 CET, issue 1748), so a restart and a timed ask do not catch the pass. After the delivery, on the
workstation first and then the laptop, `<node>/node-reads.read` of `/etc/resolv.conf` shows the one server and
§1's line; the proof that the wait outlasts the pass is §5's.
5. **The live proof.** At the next silence of the first mesh resolver, `mesh-build-go.build_go_steps` lists no Go
build step on a machine holding `node-resolver` failed with `i/o timeout` at that machine's own address. Issue
1748 is resolved once §4's read of the file holds, naming this line as where the proof will show, and reopened
if it fails.
@@ -6,11 +6,13 @@ code:
- mesh-controller internal/catalogue/seats.go, declaration.go, resolve.go
- mesh-catalog modules/systemd-resolved (the resolver, its guard and its verbs)
- mesh-catalog modules/forticlient (the VPN client's adapter)
updated: 2026-10-07
- mesh-host internal/compose/resolution, internal/network (the names check of the file, milestone resolver-wait)
updated: 2026-11-12
decisions:
- "ADR 0247"
- "ADR 0223"
- "ADR 0241"
- "ADR 0678"
---
# 50 — Split DNS on a machine with a VPN client
@@ -66,8 +68,14 @@ not share a machine.
## 2. The file and the resolver
- **The file** names the machine's private address alone, with ADR 0223's options, and begins as every
mesh resolver file does, so the uplink's `resolvers` verb and the node-engine read it as the mesh's.
- **The file** names the machine's private address alone and begins as every mesh resolver file does, so
the uplink's `resolvers` verb and the node-engine read it as the mesh's.
- **The file's options wait longer than resolved's pass** over one silent mesh resolver to the next
(ADR 0678 §1): `options timeout:3 attempts:2 edns0`, a total wait of 6 s, the timeout times the attempts, where
ADR 0223's `timeout:1 attempts:2` gave about 2 s. Its program asks only the own resolver, so it waits on that
pass; a file that lists the mesh's resolvers keeps `timeout:1 attempts:2`, because its programs try the second
after one second (ADR 0678 §2, design 08 §2). The value stays ADR 0678's 3 s per try until a read of resolved's
own server switches exists, since no read today catches the pass (milestone resolver-wait §4).
- **The uplink's holder steps back** from that path on that machine, by the controller's rule. It does not
write the file and lose a race; it is never given the file to write.
- **resolved** is given every mesh resolver as its global servers, with `~.` so they answer every name no
@@ -122,12 +130,24 @@ resolver's hold, and is therefore raised. A VPN that tells resolved its link's D
## 6. What it costs and what it does not do
- The client's search domains route full names. They do not complete short ones.
- A name no mesh resolver answers takes the file's total wait to fail on that machine: 6 s, about 4 s longer
than before (ADR 0678).
- resolved down is no names on that machine. Its `unit` health, and the node-engine's names check against
the address the file lists, say so.
- resolved should take no servers from the resolver file, because its own are configured. The live test
confirms this while a write stands.
- A container started before the resolver keeps the mesh's two resolvers until it restarts.
## How it is checked
| Check | Defends |
|---|---|
| the own resolver module's manifest test: its file names one server, the machine's private address, with §2's options | ADR 0247 §6, ADR 0678 §1 |
| the node-engine's test that composes the catalog's own resolver on a machine reads the options from the catalog's template, not a literal, and its names check bounds each ask by the file's `timeout:` | ADR 0678 §1 |
| the test over every uplink's resolver file: both mesh resolvers, `timeout:1 attempts:2` | ADR 0678 §2, design 08 §2 |
| live, on a machine holding `node-resolver`: at the next silence of its first mesh resolver no Go build step there fails on `i/o timeout` (milestone resolver-wait §5); a timed lookup waits for a read of resolved's own server switches (§4) | ADR 0678 §1 |
| each phase's *Done when* below | ADR 0247 |
## Phases
| Phase | Repository | Delivers | Done when |
@@ -135,6 +155,7 @@ resolver's hold, and is therefore raised. A VPN that tells resolved its link's D
| A — the seat | mesh-controller | `node-resolver` and its verbs; the uplink stepping back | its tests pass; the controller is running on the mesh |
| B — the resolver | mesh-catalog `systemd-resolved` | the module, its guard and verbs | its tests pass; assigned to every machine that runs the VPN client's module, each one's file names its own address, `routes` answers |
| C — the adapter | mesh-catalog `forticlient` | `split-dns` required; the adapter | its tests pass; on the laptop, connected, a company name and a mesh name both resolve and no rewrite stays raised |
| resolver-wait | mesh-catalog `systemd-resolved`, `mesh-host` | design 50 §2's options (ADR 0678), under issue 1748; its part is [`50-milestones/resolver-wait.md`](50-milestones/resolver-wait.md) | its tests pass; *How it is checked*'s live row holds |
## As built
+1 -1
View File
@@ -49,7 +49,7 @@ document is written and this one's status becomes `implemented`.
| [`46-the-conversation-with-the-operator.md`](46-the-conversation-with-the-operator.md) | **Designed.** The mesh tells and asks its operator over channels that are holders of two kinded benches, `channel` and `intake`, declaring capabilities from a fixed vocabulary; the router orders them by work context and never lowers the bar; an answer that performs an action is checked and performed by the controller, on a TOTP code or a verified Telegram sender, never on a desk click alone; operator messages addressed or answered by the mesh's responder, untrusted input kept as data, references for what words may not carry, and a recoverable factor; Telegram first, in six phases | ADR 0234, ADR 0227 |
| [`47-delivery-from-commit-to-delivered.md`](47-delivery-from-commit-to-delivered.md) | **Designed.** A delivery is one commit in one repository, from its pull request's head to every machine; a delivery group is deliveries sharing a branch name, ordered and checked as one future state; both owned by the `mesh-delivery` module with one state table, its state on the bus, every transition said, noted on the commit and shown on the pull request; the controller keeps the planner, the gate, sending and the rollout, and the core's own updates never wait for the module | ADR 0239, ADR 0238, ADR 0236 |
| [`49-the-mesh-in-domains.md`](49-the-mesh-in-domains.md) | **In progress.** Every concept belongs to one of eleven domains, which owns its one word; the glossary is organised by them and is the authority, a retired word is named on its replacement's line with its scope, and two checks hold this repository's documents and the catalog's tool descriptions to it | ADR 0244, ADR 0006, ADR 0008, ADR 0302 |
| [`50-split-dns-on-a-machine-with-a-vpn-client.md`](50-split-dns-on-a-machine-with-a-vpn-client.md) | **In progress.** A machine whose VPN client pushes servers of its own runs a resolver of its own, on a node seat, that routes the VPN's domains over its link and every other name to the mesh's resolvers; the VPN client's module carries the adapter | ADR 0247, ADR 0223 |
| [`50-split-dns-on-a-machine-with-a-vpn-client.md`](50-split-dns-on-a-machine-with-a-vpn-client.md) | **In progress.** A machine whose VPN client pushes servers of its own runs a resolver of its own, on a node seat, that routes the VPN's domains over its link and every other name to the mesh's resolvers; the VPN client's module carries the adapter | ADR 0247, ADR 0223, ADR 0678 |
| [`51-the-registries-keep-what-is-named.md`](51-the-registries-keep-what-is-named.md) | **In progress.** The artifact store, the package registry and each machine's container runtime get tools that say what they hold, what names each thing and what could go; every removal is a dry run unless a person asks and says why, and what decides is always a record | ADR 0251, ADR 0189 |
| [`52-shares-and-mounts.md`](52-shares-and-mounts.md) | **In progress.** A node shares one folder at a time through `nfs-server`, which provides it as a provision to the private network only, mapping every client to the folder's owner; a node that asks mounts it through `mounts`, a mount unit and an automount per share, so nothing mounts or fails at boot; a device that comes and goes is an occasional source whose absence is a state | ADR 0263, ADR 0051, ADR 0092 |
| [`53-the-issue-tracker.md`](53-the-issue-tracker.md) | **Designed.** The mesh keeps its issues in one module, `issues`, on the control-node: every number from one counter advanced by compare-and-set, the cycle's issue rules refused at the write, an event per change; an issue lives whole in the tracker, hq keeping no issue content (ADR 0291) | ADR 0275, ADR 0291 |