Files
mesh-controller/testdata/beside/mesh-catalog/modules/dnsmasq/module.json.captured
T
jschoubben eb72075104
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
Judge tests that read another repository against what the check clones, never the desktop's checkout (issue 432)
Tests that read ../../../mesh-catalog or ../../../mesh-host gave a verdict
that depended on what sat beside the checkout: a stale or dirty sibling
failed them on a desktop, and a missing one skipped them unseen. They now
read the clone the build seat puts in MESH_CHECK_BESIDE, failing when it is
absent there, and elsewhere a copy captured at a named commit.

The skip had hidden that the builder test read a module retired by ADR 0190.
The systemd reading test no longer counts the machine's own environment.d.
2026-10-11 02:55:04 +02:00

155 lines
10 KiB
Plaintext

{
"module": "dnsmasq",
"version": "1",
"upgrade": {
"policy": "record",
"why": "the mesh's resolver: a build that breaks it can stop a machine resolving the bus, and then neither the gate's rollback nor a push reaches it (hq ADR 0236, issue 260)"
},
"provides": [
{
"name": "wildcard-resolution",
"scope": "mesh",
"identity": false
}
],
"requires": [
"mesh-addressing"
],
"emits": [
"name.added",
"name.removed",
"restarted",
"restart-judged"
],
"own-secrets": {
"broker": "${dir:mesh-state}/broker"
},
"claims": [
{
"name": "mesh-dns-resolver",
"scope": "mesh"
}
],
"tools": [
"dnsmasq_health",
"dnsmasq_names",
"dnsmasq_resolve"
],
"listens": [
{
"name": "dns-udp",
"port": 53,
"protocol": "udp",
"from": "mesh",
"why": "one of the mesh's resolvers (ADR 0194, 0223): every node's and every container's resolver, beside any other holder of the seat — the mesh's own names answered here, a module's zone forwarded to it, the rest forwarded upstream",
"fixed": true
},
{
"name": "dns-tcp",
"port": 53,
"protocol": "tcp",
"from": "mesh",
"why": "the same names over tcp, which a resolver answers on as well and is asked for whenever an answer will not fit in a datagram. Declared because the daemon serves it: a declaration that covers one of the two protocols its own service listens on leaves the other closed while everything reports success",
"fixed": true
}
],
"resources": [
{
"id": "mesh-state",
"type": "directory",
"mode": "0700",
"place": "mesh"
},
{
"id": "package",
"type": "package",
"package": "dnsmasq"
},
{
"id": "config",
"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# This is one of the mesh's resolvers (novox/hq ADR 0194, 0223): it holds the\n# `mesh-dns-resolver` seat, which more than one machine may hold, each answering\n# the same names from the same roster. Every node and every container lists every\n# holder and no public resolver, so whichever answers first gives the one answer.\n# It holds the mesh's own names and nothing else (ADR 0191) — the roster is the\n# mesh's, rendered into each holder; no other copy lives on any machine.\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# Every zone a module answers itself (ADR 0199): one forwarding line per zone, to\n# the private address of the machine its module runs on and the port its\n# answerer is published on. Nothing in a zone is answered here; what names a zone\n# holds is the module's. Rewritten whenever a zone is declared or moves, and the\n# service restarts on it for the same reason as above.\nconf-file=/etc/mesh-resolver/zones.conf\n\n# Where it answers: this machine's private address, so every node — and every\n# container on every node, which copies its machine's resolvers — can ask; and\n# loopback, for this machine's own use. Never a LAN address: a device that is\n# not a member cannot reach what the mesh's names point at, and a LAN's resolver\n# is its router's (ADR 0194).\n#\n# Named as an ADDRESS and not as the interface that carries it, on purpose. A\n# container's query is addressed to this address but arrives on the runtime's\n# bridge, and dnsmasq checks every query against what it was told to answer on:\n# an `interface=` line admits queries by the interface they arrive on, a\n# `listen-address=` line by the address they are sent to. With `interface=mesh0`\n# alone, a query from a container was received and dropped without a word\n# (novox/hq 04-ISSUES/110). The address admits it whatever bridge it comes in on.\n#\n# bind-interfaces, and never bind-dynamic (novox/hq issue 348). bind-dynamic\n# re-reads the machine's addresses on every change of any link and closes each\n# listener whose interface that reading did not find; a reading made while the\n# container runtime deletes a bridge's virtual links fails (\"index gone\"), and\n# then every listener is closed until a later change reads cleanly. On\n# 2026-10-09 the private address was left unbound for twenty minutes, the daemon\n# running and silent. bind-interfaces binds each address once, at start, and\n# never closes it. It cannot bind an address that is not there yet, so the unit\n# waits for it (the drop-in below); a new private address restarts the unit, as\n# this file changing always did.\nbind-interfaces\nlisten-address=${machine:address}\nlisten-address=127.0.0.1\n\n# **It must never read resolv.conf to find out where to forward.** This\n# machine's resolv.conf names this resolver — so a resolver that read it for\n# upstreams would find itself, and every query it could not answer locally would\n# loop until its receive queue filled. That is not theoretical: it filled with\n# 15KB of queries and every lookup on the machine hung. no-resolv makes that loop\n# impossible: the upstreams are the two lines below, and nothing on the machine\n# can redirect them. The mesh's own names never reach them: the local= line in\n# the file above stops them here, answered or refused.\nno-resolv\nserver=1.1.1.1\nserver=8.8.8.8\n\n# Both upstreams validate DNSSEC and say so with the Authenticated Data bit,\n# and this passes that bit down rather than dropping it, which dnsmasq does\n# unless told. It does not validate itself: an answer's trust is the upstream's\n# and the path to it, which is what a forwarding resolver's trust always was.\n# Without it a program that refuses to run behind a resolver that does not\n# validate — a mail system's admin does exactly that check at start — cannot\n# use this resolver (novox/hq 04-ISSUES/171).\nproxy-dnssec\n\n# A name without a dot is never forwarded, and reverse lookups of private ranges\n# are answered here rather than asking the world who 10.x is.\ndomain-needed\nbogus-priv\n\n# **No hosts file, and no operator's files.** This machine's /etc/hosts is its\n# own — the hosts module's block and the operator's lines (ADR 0199) — and the\n# mesh's resolver answers every node, so a line written for one machine's own\n# programs must not become an answer for all of them. Every per-node resolver\n# went wrong exactly here: a hosts file read once at start, an operator's old\n# line beside the mesh's, a drop-in read from a directory nobody owned.\nno-hosts\n"
},
{
"id": "drop-ins",
"type": "directory",
"path": "/etc/systemd/system/dnsmasq.service.d",
"mode": "0755"
},
{
"id": "wait-for-address",
"type": "file",
"path": "/etc/systemd/system/dnsmasq.service.d/mesh.conf",
"mode": "0644",
"content": "# Written by the mesh (module dnsmasq, novox/hq issue 348). Replaced on every push.\n#\n# The resolver binds its addresses once, at start (bind-interfaces), so it starts only once this\n# machine's private address is there: it waits up to 80 seconds for it, and a start that found it\n# missing is tried again every ten seconds rather than left failed.\n[Unit]\nAfter=network-online.target\nWants=network-online.target\nStartLimitIntervalSec=0\n\n[Service]\nExecStartPre=/usr/bin/sh -c 'for i in $(seq 1 80); do ip -4 -o addr show to ${machine:address} | grep -q . && exit 0; sleep 1; done; echo \"${machine:address} is not on this machine\" >&2; exit 1'\nRestart=on-failure\nRestartSec=10s\n"
},
{
"id": "service",
"type": "service",
"unit": "dnsmasq.service",
"state": "running",
"boot": "enabled",
"restart-on": [
"config",
"wait-for-address",
"dnsmasq.fact-node-zones",
"dnsmasq.fact-zones"
],
"health": {
"kind": "unit"
}
},
{
"id": "watch-dir",
"type": "directory",
"path": "/var/lib/dnsmasq-watch",
"mode": "0755"
},
{
"id": "watch",
"type": "process",
"name": "dnsmasq-watch",
"artifact": "tools",
"run": [
"./dnsmasq-tools",
"watch"
],
"health": {
"kind": "tool",
"tool": "dnsmasq_health",
"interval": "20s",
"timeout": "10s",
"looks": 2,
"grace": "60s"
}
}
],
"facts": {
"node-zones": {
"path": "/etc/mesh-resolver/nodes.conf",
"template": "# Generated by the mesh. Do not edit — this file is replaced whenever a machine\n# joins or leaves, and an edit would survive until then and vanish.\n\nlocal=/{{.Suffix}}/\n# A machine's own name is a host record as well as a wildcard: asked for an IPv6 address, the\n# resolver then says the name exists and has none, where the wildcard alone said there is no such\n# name — and a resolver that reads that as final (musl, so every Alpine container) failed to find\n# the machine at all (novox/hq issue 262).\n{{range .Machines}}address=/{{.FQDN}}/{{.Address}}\nhost-record={{.FQDN}},{{.Address}}\n{{end}}"
},
"zones": {
"path": "/etc/mesh-resolver/zones.conf",
"template": "# Generated by the mesh. Do not edit — this file is replaced whenever a module declares a zone or\n# its machine moves (novox/hq ADR 0199). One forwarding line per zone, to the module answering it.\n\n{{range .Zones}}server=/{{.Zone}}/{{.Address}}#{{.Port}}\n{{end}}"
}
},
"build": {
"artifacts": [
{
"name": "tools",
"kind": "bundle",
"language": "go",
"system": "arch",
"from": "cmd/dnsmasq-tools",
"binary": "dnsmasq-tools",
"loads": [
"dnsmasq-tools"
]
}
]
}
}