diff --git a/03-DESIGN/01-to-be/50-milestones/resolver-wait.md b/03-DESIGN/01-to-be/50-milestones/resolver-wait.md new file mode 100644 index 00000000..1e85f68d --- /dev/null +++ b/03-DESIGN/01-to-be/50-milestones/resolver-wait.md @@ -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-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-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. diff --git a/03-DESIGN/01-to-be/50-split-dns-on-a-machine-with-a-vpn-client.md b/03-DESIGN/01-to-be/50-split-dns-on-a-machine-with-a-vpn-client.md index 9eb5d1e3..cdbd0f38 100644 --- a/03-DESIGN/01-to-be/50-split-dns-on-a-machine-with-a-vpn-client.md +++ b/03-DESIGN/01-to-be/50-split-dns-on-a-machine-with-a-vpn-client.md @@ -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 diff --git a/03-DESIGN/01-to-be/README.md b/03-DESIGN/01-to-be/README.md index 1e3eecd0..86feca79 100644 --- a/03-DESIGN/01-to-be/README.md +++ b/03-DESIGN/01-to-be/README.md @@ -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 |