The resolver answers on an address systemd does not hold

`127.0.0.54` is systemd-resolved's DNS *proxy* stub. The module asserted it was
free, in a comment that read as reasoned — "not .53, that is
systemd-resolved's" — and it was simply wrong: resolved holds both. dnsmasq
could not create the socket and never started.

Nothing in a unit test could have caught it. They checked the module names an
address and that the asking modules point at the same one, and all of that
passed while the daemon could not start. Only a machine knows which addresses
are spare, which is the argument for proving a module that asserts facts about
machines on a machine, before believing the assertions.

So it moves to .55, and says what that is: a convention, not a reservation. If
a future systemd takes it, this line changes and nothing else does.

The tests now derive the address from the serving module and check the two
asking modules agree with it, rather than naming it a fourth time — that fourth
place is the one nobody would think to change.

And the lab assigns `resolved-split-dns` rather than `resolv-conf`: those
machines run systemd-resolved, which owns the file. The two claim the same
thing precisely so the wrong choice is a refusal rather than a fight, and
picking the wrong one was testing the fight.
This commit is contained in:
2026-08-31 13:39:58 +02:00
parent 6eeb10f066
commit e3a2790acd
4 changed files with 46 additions and 9 deletions
+43 -6
View File
@@ -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)
}
}
}