diff --git a/examples/modules/dnsmasq.json b/examples/modules/dnsmasq.json index 15d2059..fffc274 100644 --- a/examples/modules/dnsmasq.json +++ b/examples/modules/dnsmasq.json @@ -15,7 +15,7 @@ {"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# 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 — including a container\n# on this machine — can ask.\n# 127.0.0.54 this machine's own use. Not 127.0.0.1 and not 127.0.0.53:\n# the first is where everything else expects a resolver, and the\n# second is systemd-resolved's. Taking either would be this\n# module claiming something it did not say it claims.\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.54\n\n# It answers for the mesh and forwards nothing it was not asked about. Names\n# outside the mesh are somebody else's business, and a resolver that answered\n# them would be this module taking over more than it claims.\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. 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 — including a container\n# on this machine — can ask.\n# 127.0.0.55 this machine's own use, for whatever points resolution at the\n# mesh. Not 127.0.0.1, where everything else expects a resolver.\n# Not .53 or .54 either: systemd-resolved holds BOTH — .53 is its\n# stub and .54 its proxy stub — which this module asserted was\n# free until a machine said otherwise.\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 answers for the mesh and forwards nothing it was not asked about. Names\n# outside the mesh are somebody else's business, and a resolver that answered\n# them would be this module taking over more than it claims.\ndomain-needed\nbogus-priv\n"}, {"id": "service", "type": "service", "unit": "dnsmasq.service", "state": "running", "boot": "enabled", diff --git a/examples/modules/modules_test.go b/examples/modules/modules_test.go index 530e862..622f089 100644 --- a/examples/modules/modules_test.go +++ b/examples/modules/modules_test.go @@ -89,13 +89,50 @@ func TestTheResolverNeedsToKnowNothingAboutItsMachine(t *testing.T) { t.Fatalf("it does not bind the private network's interface %q:\n%s", overlay.Interface, config) } - if !strings.Contains(config, "listen-address=127.0.0.54\n") { - t.Fatalf("it does not answer on the address the asking modules point at:\n%s", config) + // That it binds one, not which. Which address it is belongs in the manifests, where the + // asking modules can be checked against it — naming it here too would be a fourth place to + // keep in step, and the one nobody would think to change. + if !strings.Contains(config, "\nlisten-address=127.0.0.") { + t.Fatalf("it answers on no address for the machine's own use:\n%s", config) } - // Not the two addresses that belong to something else. - for _, taken := range []string{"listen-address=127.0.0.1", "listen-address=127.0.0.53"} { - if strings.Contains(config, taken) { - t.Fatalf("it takes %q, which belongs to something it did not claim:\n%s", taken, config) + // Not an address that belongs to something else. + // + // **systemd-resolved holds .53 AND .54** — the stub and the proxy stub. This module asserted + // .54 was free, in a comment that read as reasoned, and a machine said otherwise: dnsmasq + // could not start at all. A unit test cannot know which addresses a machine has spare, but it + // can hold on to what one has already told us. + for _, taken := range []string{"127.0.0.1", "127.0.0.53", "127.0.0.54"} { + if strings.Contains(config, "listen-address="+taken) { + t.Fatalf("it takes %s, which belongs to something else:\n%s", taken, config) + } + } +} + +// Everything that points resolution at the mesh points at the same place. +// +// Three files name this address — one binds it and two send queries to it — and a change to one +// of them alone is a resolver answering where nobody asks. +func TestTheAskingModulesPointAtWhereTheResolverAnswers(t *testing.T) { + serving := read(t, "dnsmasq.json").Resources[1]["content"].(string) + var at string + for _, line := range strings.Split(serving, "\n") { + if rest, found := strings.CutPrefix(strings.TrimSpace(line), "listen-address="); found { + at = rest + } + } + if at == "" { + t.Fatal("the resolver binds no address for the machine's own use") + } + for _, asking := range []string{"resolved-split-dns.json", "resolv-conf.json"} { + m := read(t, asking) + var mentions bool + for _, r := range m.Resources { + if content, ok := r["content"].(string); ok && strings.Contains(content, at) { + mentions = true + } + } + if !mentions { + t.Fatalf("%s does not point at %s, where the resolver answers", asking, at) } } } diff --git a/examples/modules/resolv-conf.json b/examples/modules/resolv-conf.json index 5631573..d7b161f 100644 --- a/examples/modules/resolv-conf.json +++ b/examples/modules/resolv-conf.json @@ -7,6 +7,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.54\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 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"} ] } diff --git a/examples/modules/resolved-split-dns.json b/examples/modules/resolved-split-dns.json index cbe99ea..dd3ea6d 100644 --- a/examples/modules/resolved-split-dns.json +++ b/examples/modules/resolved-split-dns.json @@ -10,7 +10,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.54 is where the mesh's resolver answers on every machine — a name the\n# mesh chose, so this file needs to know nothing about this particular one.\n[Resolve]\nDNS=127.0.0.54\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.\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"}, {"id": "resolved", "type": "service", "unit": "systemd-resolved.service", "state": "running", "boot": "enabled", "restart-on": ["route"]}