diff --git a/examples/modules/dnsmasq.json b/examples/modules/dnsmasq.json index fffc274..7ed1ff6 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.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"}, + "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 .53 or .54: 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# 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 — 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"}, {"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 622f089..8179627 100644 --- a/examples/modules/modules_test.go +++ b/examples/modules/modules_test.go @@ -192,3 +192,25 @@ func TestTheExamplesAreWellFormed(t *testing.T) { } } } + +// The resolver must not look up its own upstreams. +// +// Whatever points a machine at the mesh writes that address into resolv.conf, so a resolver that +// read it would find itself — and every query it could not answer locally would loop until its +// receive queue filled. It did: 15KB of queries backed up and every lookup on the machine hung. +// +// It needs no upstream because it is never asked for anything else: the asking module routes only +// the mesh's suffix here and leaves the rest where the machine already sent it. +func TestTheResolverDoesNotAskItselfForUpstreams(t *testing.T) { + config := read(t, "dnsmasq.json").Resources[1]["content"].(string) + if !strings.Contains(config, "\nno-resolv\n") { + t.Fatalf("it reads resolv.conf for upstreams, which now points at itself:\n%s", config) + } + // And names no upstream of its own: choosing one would send every query this machine cannot + // answer somewhere nobody agreed to. + for _, line := range strings.Split(config, "\n") { + if strings.HasPrefix(strings.TrimSpace(line), "server=") { + t.Fatalf("it forwards to %q, which is not the mesh's to choose", line) + } + } +}