Files
hq/01-RESEARCH/033-split-dns-with-a-vpn-client/02-options.md
T
jochen a28da1734b Graduate research 034: the mesh in domains, one word per thing, checked
ADR 0244 sorts the mesh's concepts into ten domains (ADR 0006's contexts
carry over as domains), keeps machine and node as distinct words, and
makes the glossary the authority with every retired word on its
replacement's Not: line. To-be 49 draws the domains; the glossary is
reorganised by them, its two contradictions removed and the missing
words added. words.py now fails on a retired word in running prose and
on a word defined twice, so the rule is enforced rather than believed;
the 63 documents it failed on are reworded here, and research 034 is
kept as the record of the words it studied.
2026-10-07 19:58:39 +02:00

114 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 02 — Options
What any answer must give, from the [evidence](01-evidence.md) and ADR 0223:
- **R1** mesh names resolve on the machine and in its containers while the VPN is up;
- **R2** the corporate domains resolve through the VPN's servers while it is up;
- **R3** public names resolve, VPN up or down;
- **R4** no listed set of servers can give two different answers to one name — musl asks every listed
server at once and takes the first reply, glibc takes the first server's "no such name" as final
([issue 262](../../04-ISSUES/262-an-alpine-container-could-not-find-a-machine-by-its-mesh-name/00-report.md),
ADR 0223);
- **R5** the VPN client keeps working unmodified — it is the employer's software, configured by the
employer's gateway; the mesh cannot change what it pushes or how it writes;
- **R6** no change on a machine that runs no VPN.
A constraint every option meets: **the VPN client always writes `/etc/resolv.conf` itself**, even with
systemd-resolved running. No option makes it stop; each decides what happens next.
## 1. systemd-resolved with per-link DNS and routing domains
resolved runs on the machine. The `mesh0` link gets the two mesh resolvers with the routing domain
`~internal` and the default route (`~.`); the tunnel link gets the VPN's two servers and its eight
domains as routing domains. resolved sends each question to the link whose domain matches longest, the
rest to the default route.
- **R1–R3 met**, by routing rather than by listing: one answer per name.
- **Containers cannot reach resolved's stub on `127.0.0.53`.** A container on the node-engine network can; one
on a bridge cannot — it reads the machine's file and gets an address that is its own loopback. resolved
can listen on further addresses (`DNSStubListenerExtra=`): on the machine's private mesh address, which
ADR 0223 already relies on being reachable from containers on a holder. The machine's file then lists
that one address. **R4 met**: one server listed.
- **Who gives the tunnel link its servers?** Not the VPN client (it never sets per-link DNS) and not
NetworkManager (the tunnel is not its device). Something must read what the VPN pushed — and the VPN
client writes exactly that, in its file, within a second of connecting. A path watch on the resolver
file, declared by the uplink's holder, can read the file's `nameserver` and `search` lines, give them to
the tunnel link with `resolvectl dns` and `resolvectl domain`, and write the mesh's file back at once —
not at the next reconcile. When the tunnel goes, resolved forgets its link's configuration by itself.
- **The VPN client's restore at disconnect** puts back the mesh's file (or an older one: the node-engine
corrects the content, as today). Nothing points at a link that is gone.
- **Cost:** a daemon on the machine (already installed; part of systemd), its configuration, the watch.
- **For ADR 0223:** it is the local forwarder 0223 rejected (its option 2), on fewer machines and with
its three objections met: not on every machine — only on one that declares a VPN client; reachable
from containers — on the private address; and it holds no copy of the mesh's names — every `.internal`
question still goes to the two resolvers, so nothing can disagree with the truth.
- **R6 met** if it is scoped to machines that declare a VPN client.
## 2. The uplink's holder adds the VPN's servers to the mesh's file
When the tunnel appears, the holder writes the mesh's two resolvers *and* the VPN's two servers, and the
VPN's search domains.
- **R4 broken, by construction.** The VPN's servers answer "no such name" for `.internal`; the mesh's
resolvers forward a corporate name to the public internet, which answers "no such name" or a public
record. glibc stops at the first listed server's answer, musl takes whichever is fastest. Whatever order
is chosen, one kind of name fails — the exact fault ADR 0223 removed the public resolver for.
- Rejected. It only works with something that routes by domain in front — which is option 1 or 5.
## 3. The mesh's resolvers forward the corporate domains while the VPN is up
- **They cannot reach the VPN's servers**: those sit behind the laptop's tunnel, routed only on the
laptop. Forwarding through the laptop would make the mesh's resolvers depend on one laptop's session.
- It would also put an employer's zones into the mesh's shared configuration, answered for every machine.
- Rejected.
## 4. Stand back: detect the VPN and defer
While the resolver file carries the VPN client's marker and its backup holds the mesh's file, the
node-engine does not correct it, and the machine's health says *deferred to a VPN client* rather than
*rewritten by another program*. On disconnect the client restores the mesh's file.
- **R2, R3 met; R1 broken for the whole session**: no mesh name resolves on the laptop, nor in its
containers. The node-engine reaches the bus by name; agents and tools on the laptop lose the mesh.
- **Cheap**: no daemon, one rule in the node-engine (and the content check on restore stays).
- Today's behaviour is the opposite trade — R1 met after a few minutes, R2 broken for the rest of the
session — and the evidence shows the operator living with it by reconnecting.
- Acceptable only as a stated, chosen degradation for a machine where the owner's work comes first;
better than the fight, worse than routing. **Useful as the fallback** of option 1 while it is not
built, if the operator prefers work names to mesh names.
## 5. A local forwarder of the mesh's own (dnsmasq) on the VPN machine
As option 1, with the catalogue's resolver program in place of resolved: `server=/<corporate domain>/…`
lines for the VPN's domains, everything else to the two mesh resolvers, listening on the private address.
- Meets R1–R4 like option 1.
- **Costs more than option 1**: a package to declare, and a configuration file to rewrite and a
process to reload on every connect and disconnect, where resolved takes per-link settings live over its
own interface and drops them with the link. dnsmasq is already the mesh's resolver module, which makes
"a second copy of the resolver on this machine" easy to misread as a third mesh resolver.
- Kept as the fallback for a machine without systemd.
## 6. Replace the VPN client
openfortivpn, through NetworkManager's plugin, hands the VPN's DNS to NetworkManager instead of writing
the file; NetworkManager with `dns=systemd-resolved` then gives resolved per-link servers and domains —
option 1 with no watch at all.
- **Not the mesh's to decide** (R5): the employer's gateway may require its own client (posture checks,
single sign-on through the vendor's flow). It would also hand the file back to NetworkManager, against
ADR 0117's `dns=none`.
- Recorded so the option is not rediscovered; not proposed.
## Summary
| | R1 mesh | R2 corporate | R3 public | R4 one answer | R6 others untouched | New on the machine |
|---|---|---|---|---|---|---|
| 1 resolved, per link | yes | yes | yes | yes | yes, if scoped | resolved, a path watch |
| 2 add the VPN's servers | some | some | yes | **no** | yes | nothing |
| 3 forward from the mesh | yes | **no** | yes | yes | **no** | nothing |
| 4 defer | **no** | yes | yes | yes | yes | one rule |
| 5 dnsmasq locally | yes | yes | yes | yes | yes, if scoped | a package, a reload per connect |
| 6 replace the client | yes | yes | yes | yes | yes | not the mesh's call |
| today (the fight) | after minutes | **no**, after minutes | mostly | yes | yes | — |