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

7.4 KiB
Raw Blame History

02 — Options

What any answer must give, from the evidence 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, 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.

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.

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 —