dnsmasq: forward outside names (systemd-resolved stub or resolv.conf)
CI / agent (push) Successful in 3s
CI / site (push) Successful in 2s
CI / go (push) Successful in 35s

Debian's dnsmasq looks for /run/dnsmasq/resolv.conf, which only resolvconf
writes; without it every name outside the cluster domain was refused, so
nodes could not resolve mirrors or site services.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Ilker ManapandClaude Opus 5.5 committed 2026-10-28 11:07:07 +01:00
1 parent 77894b7d57
commit eb245b1d51
3 files changed
+73 -1

No files matched your search

+3
View File
@@ -66,6 +66,9 @@ publish a tag that has no section here.
- Release notes from `CHANGELOG.md`.
### Fixed
- Rack controllers' dnsmasq had no upstream DNS on Debian (it looks for a resolvconf file that is
usually missing) and refused every name outside the cluster domain; the generated configuration
now forwards to systemd-resolved's stub (keeping split DNS) or the host's resolv.conf servers.
- Virtual BMC addresses are configured in netplan on the lab PXE server (they were lost on a
network reconfigure, which made every BMC call fail).
- Commissioning console no longer shows a failing autologin (`noautologin`).
+19
View File
@@ -2,6 +2,8 @@ package main
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)
@@ -224,3 +226,20 @@ func TestDnsmasqIPv6(t *testing.T) {
t.Errorf("router lifetime 0 although the rack is the gateway:\n%s", conf)
}
}
func TestDnsmasqUpstream(t *testing.T) {
d := t.TempDir()
stub, resolv := filepath.Join(d, "stub"), filepath.Join(d, "resolv")
if got := dnsmasqUpstream(stub, resolv, nil); got != "" {
t.Errorf("no files: %q", got)
}
_ = os.WriteFile(resolv, []byte("# x\nnameserver 127.0.0.1\nnameserver 10.213.77.1\nnameserver 10.0.0.1\nnameserver 1.1.1.1\nsearch lan\n"), 0o644)
got := dnsmasqUpstream(stub, resolv, []string{"10.213.77.1/24"})
if !strings.Contains(got, "no-resolv\nserver=10.0.0.1\nserver=1.1.1.1\n") || strings.Contains(got, "127.0.0.1") || strings.Contains(got, "10.213.77.1") {
t.Errorf("resolv.conf upstream:\n%s", got)
}
_ = os.WriteFile(stub, []byte("nameserver 127.0.0.53\n"), 0o644)
if got := dnsmasqUpstream(stub, resolv, nil); !strings.Contains(got, "server=127.0.0.53") {
t.Errorf("resolved stub: %q", got)
}
}
+51 -1
View File
@@ -590,7 +590,7 @@ func applyRackState(st *rackState, tftp, dnsDir string) (changed []string, err e
note("hosts", ch, e)
conf := filepath.Join(dnsDir, "conf.d", "rollcall.conf")
if st.DnsmasqConf != "" {
ch, e = writeIfChanged(conf, st.DnsmasqConf, 0o644)
ch, e = writeIfChanged(conf, st.DnsmasqConf+dnsmasqUpstream("/run/systemd/resolve/stub-resolv.conf", "/etc/resolv.conf", localAddrs()), 0o644)
note("dnsmasq.conf", ch, e)
} else if _, e := os.Stat(conf); e == nil {
note("-dnsmasq.conf", true, os.Remove(conf))
@@ -873,3 +873,53 @@ func (a *App) handleRackDHCP(w http.ResponseWriter, r *http.Request) {
}
fmt.Fprint(w, st.DnsmasqConf, "\n# --- hosts.dhcp\n", st.HostsDHCP)
}
// dnsmasqUpstream tells dnsmasq where to forward names it does not answer
// itself. Without it Debian's dnsmasq looks for a resolvconf-made file that
// usually does not exist and refuses every outside name, so nodes could not
// resolve mirrors or site services. With systemd-resolved the stub is used, so
// its per-link routing (split DNS such as ~int.example.com) keeps working;
// otherwise the host's resolv.conf servers, never ourselves.
func dnsmasqUpstream(stub, resolv string, own []string) string {
if _, err := os.Stat(stub); err == nil {
return "# upstream: systemd-resolved on this host (keeps its split DNS)\nno-resolv\nserver=127.0.0.53\n"
}
b, err := os.ReadFile(resolv)
if err != nil {
return ""
}
mine := map[string]bool{}
for _, a := range own {
mine[stripPrefix(a)] = true
}
var servers []string
for _, line := range strings.Split(string(b), "\n") {
f := strings.Fields(line)
if len(f) < 2 || f[0] != "nameserver" {
continue
}
ip, ok := parseAddr(f[1])
if !ok || mine[ip.String()] || (ip.IsLoopback() && ip.String() != "127.0.0.53") {
continue
}
servers = append(servers, ip.String())
}
if len(servers) == 0 {
return ""
}
out := "# upstream: this host's resolv.conf\nno-resolv\n"
for _, sv := range servers {
out += "server=" + sv + "\n"
}
return out
}
// localAddrs: this host's interface addresses (CIDR form).
func localAddrs() []string {
var out []string
addrs, _ := net.InterfaceAddrs()
for _, a := range addrs {
out = append(out, a.String())
}
return out
}