diff --git a/modules/dnsmasq/client.ts b/modules/dnsmasq/client.ts index d0c8954..ed6a5be 100644 --- a/modules/dnsmasq/client.ts +++ b/modules/dnsmasq/client.ts @@ -1,6 +1,7 @@ -// dnsmasq's own code, living in the module (novox/hq ADR 0039). dnsmasq here is a pure resolver: it -// answers the mesh's generated wildcard names (..) and forwards nothing. So -// its code reads what it was told to answer, and can resolve through itself to prove that it does. +// dnsmasq's own code, living in the module (novox/hq ADR 0039). dnsmasq here answers the mesh's +// generated wildcard names (..) itself and forwards everything else to fixed +// upstreams — the predecessor's arrangement, so the machine and its containers ask it for the world. +// Its code reads what it was told to answer, and can resolve through itself to prove that it does. import { readFile } from "node:fs/promises"; import { Resolver } from "node:dns/promises"; @@ -19,13 +20,14 @@ export class DnsmasqClient { /** * Build from the environment. Both values are node-local facts with mesh-chosen defaults — the - * wildcard file the module is sent, and the loopback address its config listens on — so there is - * nothing to be unconfigured about; it never throws. + * wildcard file the module is sent, and the loopback address its config listens on (127.0.0.1, + * where the predecessor's resolver answered and every machine's resolv.conf already points) — so + * there is nothing to be unconfigured about; it never throws. */ static fromEnv(env: NodeJS.ProcessEnv = process.env): DnsmasqClient { return new DnsmasqClient( env.MESH_DNSMASQ_RESOLVER_PATH ?? "/etc/mesh-resolver/nodes.conf", - env.MESH_DNSMASQ_ADDRESS ?? "127.0.0.55", + env.MESH_DNSMASQ_ADDRESS ?? "127.0.0.1", ); } @@ -39,7 +41,8 @@ export class DnsmasqClient { } const names: AnsweredName[] = []; for (const line of text.split("\n")) { - // dnsmasq wildcard syntax the mesh writes: address=/./
+ // dnsmasq wildcard syntax the mesh writes: address=/./
. The file also + // carries local=//, which names no machine and is not matched here. const match = line.match(/^address=\/([^/]+)\/(.+)$/); if (match) names.push({ name: match[1], address: match[2] }); } diff --git a/modules/dnsmasq/module.json b/modules/dnsmasq/module.json index 772262e..3442b37 100644 --- a/modules/dnsmasq/module.json +++ b/modules/dnsmasq/module.json @@ -4,6 +4,9 @@ "provides": [ "wildcard-resolution" ], + "requires": [ + "mesh-addressing" + ], "emits": [ "module.dnsmasq.name.added", "module.dnsmasq.name.removed" @@ -22,7 +25,7 @@ "port": 53, "protocol": "udp", "from": "mesh", - "why": "names under every machine in this mesh, for this machine and what it runs", + "why": "every name for this machine and what it runs — the mesh's own answered here, the rest forwarded", "fixed": true } ], @@ -43,7 +46,16 @@ "type": "file", "path": "/etc/dnsmasq.conf", "mode": "0644", - "content": "# Managed by the mesh. dnsmasq's own defaults are replaced whole rather than\n# patched, because this module owns the file and a patch would leave whatever\n# was there before to be discovered later.\n\n# What the mesh computed: one wildcard per machine, its name and everything\n# under it. Rewritten whenever a machine joins or leaves, which is why the\n# service below reflects it.\nconf-file=/etc/mesh-resolver/nodes.conf\n\n# Where it answers. Both are names the mesh chose, so this file needs to know\n# nothing about this particular machine:\n#\n# mesh0 the private network, so anything on it \u2014 including a container\n# on this machine \u2014 can ask.\n# 127.0.0.55 this machine's own use, for whatever points resolution at the\n# mesh. Not .53 or .54: systemd-resolved holds BOTH \u2014 .53 is its\n# stub and .54 its proxy stub \u2014 which this module asserted was\n# free until a machine said otherwise.\n#\n# Listening on a loopback address makes dnsmasq take the rest of\n# loopback with it, 127.0.0.1 included. That is why this module\n# claims `the-dns-port`: it takes the machine's DNS port, and\n# saying it takes only one address would be the same kind of\n# comfortable claim that .54 was free.\n#\n# .55 is a convention and not a reservation. If a future systemd\n# takes it, this line changes and nothing else does, which is the\n# reason it is written once here rather than in each module that\n# points at it.\n#\n# bind-dynamic rather than bind-interfaces: mesh0 does not exist until the\n# machine is on the private network, and binding an interface that is not there\n# yet fails to start rather than waiting for it.\nbind-dynamic\ninterface=mesh0\nlisten-address=127.0.0.55\n\n# **It forwards nothing, and must not read resolv.conf to find out where to.**\n# Whatever points this machine at the mesh writes its own address into\n# resolv.conf \u2014 so a resolver that read it for upstreams would find itself,\n# and every query it could not answer locally would loop until its receive\n# queue filled. That is not theoretical: it filled with 15KB of queries and\n# every lookup on the machine hung.\n#\n# It needs no upstream because it is never asked for anything else: the\n# asking module routes only the mesh's suffix here and leaves the rest\n# wherever the machine already sent it.\nno-resolv\ndomain-needed\nbogus-priv\n" + "content": "# Managed by the mesh. dnsmasq's own defaults are replaced whole rather than\n# patched, because this module owns the file and a patch would leave whatever\n# was there before to be discovered later.\n\n# What the mesh computed: one wildcard per machine — its name and everything\n# under it — and the mesh's own suffix as a local domain, so a name under it is\n# answered here or not at all and is never asked upstream. Rewritten whenever a\n# machine joins or leaves, which is why the service below restarts on it: a\n# reload makes dnsmasq re-read hosts files, not its configuration, and a\n# wildcard is configuration.\nconf-file=/etc/mesh-resolver/nodes.conf\n\n# Where it answers. Both are names the mesh chose, so this file needs to know\n# nothing about this particular machine:\n#\n# mesh0 the private network, so anything on it can ask — including\n# this machine's containers. This module writes the runtime's\n# `dns` key into its own configuration file, beside whatever the\n# machine had there (novox/hq ADR 0102), naming this address: a\n# container cannot reach the machine's loopback, and a runtime\n# whose host resolves at loopback falls back to a public resolver\n# and never sees a mesh name. The runtime reads that key when it\n# starts and not on a reload, and a restart stops every container\n# on the machine, so this module orders neither: the key holds for\n# every container created after the runtime next starts. On the\n# machine this replaces the predecessor wrote the same value, so\n# nothing there is waiting on it.\n# 127.0.0.1 this machine's own use. The predecessor's resolver answered\n# here, and the resolv.conf it wrote on every machine says so;\n# that file stays in force on an adopted machine until the mesh's\n# module for it is taken, so the resolver has to answer where the\n# machine already asks or the machine loses DNS the moment this\n# module is taken. Not .53 or .54: systemd-resolved holds BOTH —\n# .53 is its stub and .54 its proxy stub — and neither is .1, so\n# the two coexist on a machine that runs it. This module used to\n# answer on 127.0.0.55 instead: a convention of its own, beside\n# the one every machine already followed. One address, this one,\n# and the modules that point a machine at the mesh name the same.\n#\n# Whatever address it listens on, it takes the machine's DNS port.\n# That is why this module claims `the-dns-port`.\n#\n# bind-dynamic rather than bind-interfaces: mesh0 does not exist until the\n# machine is on the private network, and binding an interface that is not there\n# yet fails to start rather than waiting for it.\nbind-dynamic\ninterface=mesh0\nlisten-address=127.0.0.1\n\n# **It must never read resolv.conf to find out where to forward.** Whatever\n# points this machine at the mesh writes this resolver's own address there — so\n# a resolver that read it for upstreams would find itself, and every query it\n# could not answer locally would loop until its receive queue filled. That is\n# not theoretical: it filled with 15KB of queries and every lookup on the\n# machine hung. no-resolv is what makes that loop impossible: the upstreams are\n# the two lines below, and nothing on the machine can redirect them.\n#\n# It forwards, because it is now asked for everything. The module that points\n# this machine at the mesh names this resolver alone — as the predecessor's\n# did — so the host and every container resolve the world through it. The\n# upstreams are the ones the predecessor's module shipped as its defaults. The\n# mesh's own names never reach them: the local= line in the file above stops\n# them here, answered or refused.\nno-resolv\nserver=1.1.1.1\nserver=8.8.8.8\n\n# A name without a dot is never forwarded — a bare hostname is answered from\n# /etc/hosts or not at all — and reverse lookups of private ranges are answered\n# here rather than asking the world who 10.x is.\ndomain-needed\nbogus-priv\n" + }, + { + "id": "runtime-dns", + "type": "file", + "path": "/etc/docker/daemon.json", + "mode": "0644", + "merge": "json", + "into": "json", + "content": "{\"dns\": [\"${machine:address}\"]}\n" }, { "id": "service", diff --git a/modules/dnsmasq/package.json b/modules/dnsmasq/package.json index 4e18a47..dbf6835 100644 --- a/modules/dnsmasq/package.json +++ b/modules/dnsmasq/package.json @@ -1,7 +1,7 @@ { "name": "@novox/module-dnsmasq", "version": "0.1.0", - "description": "dnsmasq — the mesh's resolver: answers wildcard node names. Its tools and events live here.", + "description": "dnsmasq — the mesh's resolver: answers wildcard node names and forwards the rest upstream. Its tools and events live here.", "type": "module", "private": true, "dependencies": { "@novox/mesh-sdk": "^0.1.0" }, diff --git a/modules/resolv-conf/module.json b/modules/resolv-conf/module.json index 01cbba6..520e864 100644 --- a/modules/resolv-conf/module.json +++ b/modules/resolv-conf/module.json @@ -8,6 +8,6 @@ "resources": [ {"id": "resolv", "type": "file", "path": "/etc/resolv.conf", "mode": "0644", - "content": "# Managed by the mesh.\n#\n# For a machine where nothing else owns this file. On one where systemd-resolved\n# or NetworkManager does, assign that module instead — this one and those claim\n# the same thing, so the mesh refuses the pair rather than letting them take\n# turns overwriting each other, which is the failure this claim exists to stop.\n#\n# The mesh's resolver first, because it answers only the mesh's names and\n# forwards nothing: a query it does not recognise falls through to the next\n# line rather than being answered wrongly.\nnameserver 127.0.0.55\n\n# And what this machine used before. Replace this line with the resolver this\n# machine should use for everything that is not the mesh — it is not the mesh's\n# to choose, and a public one written here by default would send every query\n# this machine makes somewhere nobody agreed to.\nnameserver 127.0.0.53\n"} + "content": "# Managed by the mesh.\n#\n# For a machine where nothing else owns this file. On one where systemd-resolved\n# or NetworkManager does, assign that module instead — this one and those claim\n# the same thing, so the mesh refuses the pair rather than letting them take\n# turns overwriting each other, which is the failure this claim exists to stop.\n#\n# The mesh's resolver, and only it — the one line the predecessor wrote on every\n# machine it set up. It answers the mesh's names itself and forwards everything\n# else to upstreams named in its own configuration, never read from this file.\n# This file used to carry a second nameserver as a placeholder for \"whatever\n# this machine used before\"; that was never a fallback for names the mesh does\n# not know — a resolver's second line is asked only when the first does not\n# answer at all — and now that the first answers everything it would be a line\n# nothing ever reached.\nnameserver 127.0.0.1\noptions edns0\n"} ] } diff --git a/modules/resolved-split-dns/module.json b/modules/resolved-split-dns/module.json index 2eb1eff..246ba83 100644 --- a/modules/resolved-split-dns/module.json +++ b/modules/resolved-split-dns/module.json @@ -11,7 +11,7 @@ {"id": "route", "type": "file", "path": "/etc/systemd/resolved.conf.d/mesh.conf", "mode": "0644", - "content": "# Managed by the mesh.\n#\n# **Only the mesh's names.** The tilde makes this a routing domain rather than a\n# search domain: queries under it go to the resolver below, and everything else\n# keeps going wherever this machine already sent it. A resolver that took over\n# all of DNS would be this module claiming the machine's whole network, which\n# is not what it says it claims.\n#\n# 127.0.0.55 is where the mesh's resolver answers on every machine — a fixed\n# address, so this file needs to know nothing about this particular machine.\n# systemd-resolved holds .53 and .54, which is why it is neither.\n[Resolve]\nDNS=127.0.0.55\nDomains=~internal\n"}, + "content": "# Managed by the mesh.\n#\n# **Only the mesh's names.** The tilde makes this a routing domain rather than a\n# search domain: queries under it go to the resolver below, and everything else\n# keeps going wherever this machine already sent it. A resolver that took over\n# all of DNS would be this module claiming the machine's whole network, which\n# is not what it says it claims. The mesh's resolver can forward the rest too;\n# this module is for a machine that wants systemd-resolved to stay in charge of\n# that, and only lends it the mesh's suffix.\n#\n# 127.0.0.1 is where the mesh's resolver answers on every machine — a fixed\n# address, so this file needs to know nothing about this particular machine.\n# systemd-resolved holds .53 and .54 itself, which is why the resolver is on\n# neither, and why the two coexist here.\n[Resolve]\nDNS=127.0.0.1\nDomains=~internal\n"}, {"id": "resolved", "type": "service", "unit": "systemd-resolved.service", "state": "running", "boot": "enabled", "restart-on": ["route"]}