Hold the uplinks' resolver file here and the resolver's own configuration in its module (issue 1966)
mesh/delivery merged: source.main moved to novox/mesh-sdk@4b9bac9c73a9
mesh/delivery-group group fix/1966-resolver-test merged: every member is merged
mesh/check checking a330d7b0da16 on 4b9bac9c73a9 | novox/mesh-sdk#179 in group fix/1966-resolver-test

The literal server=1.1.1.1 broke on every pull request when the upstreams became settings; the dnsmasq configuration is checked in the module that owns it.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-09 20:59:46 +01:00
parent 0177432196
commit a330d7b0da
+9 -76
View File
@@ -5,83 +5,16 @@ import (
"testing" "testing"
) )
// The catalogue's resolver modules as they are, parsed by the real parser and composed as a // The resolver module's own configuration is held by the module's tests in the catalogue (novox/hq issue 1966): a
// machine would receive them (hal dnsmasq-app conversion, novox/hq 08-connectivity). // file moves with its settings, and a literal here broke on every pull request when the upstreams became settings.
// // What stays is what the uplink modules write, which names the mesh's resolvers and is the catalogue's across
// The predecessor's resolver answered every name on a machine: the mesh's own itself, the rest // modules.
// forwarded to two fixed upstreams, with the machine's resolv.conf naming it alone and the
// container runtime pointed at its private-network address. These hold the mesh's modules to the
// same arrangement, and to the two things a resolver here must never do — read resolv.conf for
// its upstreams, or take an address systemd-resolved holds.
// Its configuration forwards to the upstreams the predecessor's module shipped, and gets them from // The file that decides what the machine asks names every one of the mesh's resolvers, by
// nowhere else: `no-resolv` is what makes the documented loop — the resolver finding its own // address, from the seat's holders, and no public one (ADR 0223): a resolver library that asks
// address in resolv.conf and becoming its own upstream — impossible. // every listed server at once takes the first reply, and a public "no such name" for a mesh name
func TestTheResolverForwardsToFixedUpstreamsAndNeverReadsResolvConf(t *testing.T) { // won it. Written by every uplink module, since the uplink's holder owns the file.
m := catalogueManifest(t, "dnsmasq") func TestEveryUplinkNamesTheMeshsResolversAndNoOther(t *testing.T) {
var config string
for _, r := range m.Resources {
if r["id"] == "config" {
config, _ = r["content"].(string)
}
}
if config == "" {
t.Fatal("the resolver has no configuration file")
}
for _, want := range []string{
"\nno-resolv\n", "\nserver=1.1.1.1\n", "\nserver=8.8.8.8\n",
// The private address and loopback, never a LAN's (novox/hq ADR 0194): a device that is not a
// member cannot reach what the mesh's names point at.
"\nlisten-address=127.0.0.1\n", "\nlisten-address=${machine:address}\n",
// No hosts file and no operator's files: the mesh's resolver answers every node (ADR 0199).
"\nno-hosts\n",
"\nconf-file=" + m.Facts["zones"].Path + "\n",
"\ndomain-needed\n", "\nbogus-priv\n",
"\nconf-file=" + m.Facts["node-zones"].Path + "\n",
} {
if !strings.Contains(config, want) {
t.Errorf("the resolver's configuration lacks %q:\n%s", strings.TrimSpace(want), config)
}
}
// Bound to each address once, at start, and never by bind-dynamic (novox/hq issue 348): bind-dynamic
// re-reads the machine's addresses on every change of any link and closes each listener that reading
// did not find, and a reading made while the container runtime tears down a bridge's virtual links
// fails, so the private address was left unbound for twenty minutes, the daemon running and silent.
// The catalogue's main binds once since mesh-catalog PR 161 (105cc7c0).
if !strings.Contains(config, "\nbind-interfaces\n") {
t.Errorf("the resolver's configuration does not bind its addresses once (bind-interfaces):\n%s", config)
}
if strings.Contains(config, "\nbind-dynamic\n") {
t.Errorf("the resolver's configuration says bind-dynamic, which closes its listeners when a bridge is "+
"torn down (novox/hq issue 348):\n%s", config)
}
// By address and never by interface: dnsmasq admits a query by the interface it arrives on
// when told one, and a container's query to the private address arrives on the runtime's
// bridge — `interface=mesh0` dropped every such query, silently (novox/hq issue 110).
for _, line := range strings.Split(config, "\n") {
if strings.HasPrefix(line, "interface=") {
t.Errorf("the resolver answers by interface, so a container's query on a bridge is dropped: %s", line)
}
}
// Not .53 or .54, which systemd-resolved holds; and not .55 any more, which was a convention
// beside the one every machine already followed — the predecessor's resolv.conf says .1.
for _, taken := range []string{"127.0.0.53", "127.0.0.54", "127.0.0.55"} {
if strings.Contains(config, "listen-address="+taken) {
t.Errorf("the resolver listens on %s", taken)
}
}
// Never a directory or a file the operator keeps: a line written for one machine's programs would
// become an answer for every node (ADR 0199).
for _, never := range []string{"conf-dir=", "addn-hosts=", "listen-address=${setting:"} {
if strings.Contains(config, never) {
t.Errorf("the mesh's resolver still reads or listens on %q", never)
}
}
// And the file that decides what the machine asks names every one of the mesh's resolvers, by
// address, from the seat's holders, and no public one (ADR 0223): a resolver library that asks
// every listed server at once takes the first reply, and a public "no such name" for a mesh name
// won it. Written by every uplink module, since the uplink's holder owns the file.
for _, uplink := range uplinks { for _, uplink := range uplinks {
fact, ok := catalogueManifest(t, uplink).Facts["resolvers"] fact, ok := catalogueManifest(t, uplink).Facts["resolvers"]
if !ok || fact.Path != "/etc/resolv.conf" { if !ok || fact.Path != "/etc/resolv.conf" {