Publish DHCP clients' names in DNS

router-webui leases-dns reads Kea's leases and publishes <name>.<domain>
with reverse records in the running Unbound (router.dns.dynamicLeases).
Names the configuration publishes are never taken over; a client's name
is cut to its first label and kept inside the local domain; of two
clients the newer lease wins. The DNS page switches it on and shows what
is published and what was skipped; router-setup enables it with a domain.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
This commit is contained in:
2026-10-30 00:18:01 +01:00
co-authored by Claude Sonnet 5.5
parent 84f6002d05
commit 2eab735f59
13 changed files with 711 additions and 7 deletions
+51
View File
@@ -6,6 +6,7 @@ import (
"regexp"
"strings"
"testing"
"time"
)
func withDomain(t *testing.T) Settings {
@@ -145,3 +146,53 @@ func TestDNSPage(t *testing.T) {
t.Error("the network form dropped the DNS records")
}
}
func TestDynamicLeasesOnThePage(t *testing.T) {
a, sys := newTestApp(t)
a.sys.LeasesDNSDir = t.TempDir()
h := a.routes()
cookie := do(h, "POST", "/login", url.Values{"password": {"hunter2hunter2"}}, nil).Result().Cookies()[0]
token := func() string {
body := do(h, "GET", "/dns", nil, cookie).Body.String()
return regexp.MustCompile(`name="csrf" value="([^"]+)"`).FindStringSubmatch(body)[1]
}
save := func(extra ...string) {
form := url.Values{"csrf": {token()}, "recs": {"0"}, "zones": {"0"}}
for i := 0; i < len(extra); i += 2 {
form.Set(extra[i], extra[i+1])
}
if rec := do(h, "POST", "/dns", form, cookie); rec.Code != http.StatusSeeOther {
t.Fatalf("save: %d\n%s", rec.Code, rec.Body)
}
}
save("dynamic", "on")
if s, _ := loadSettings(sys.settingsPath()); !s.dynamicLeases() {
t.Fatal("not switched on")
}
body := do(h, "GET", "/dns", nil, cookie).Body.String()
if !strings.Contains(body, `name="dynamic" checked`) || !strings.Contains(body, "apply the settings so the service starts") {
t.Errorf("the page after switching on:\n%s", body)
}
// What the running service reports is shown.
writeDynStatus(a.sys.LeasesDNSDir, dynStatus{
Published: []DynRecord{{Name: "phone.home.example.org", Address: "192.168.1.101"}},
Skipped: []DynSkip{{Hostname: "nas", Address: "192.168.1.102", Reason: "the configuration has a record with this name"}},
Updated: time.Now(),
})
body = do(h, "GET", "/dns", nil, cookie).Body.String()
for _, want := range []string{"phone.home.example.org", "192.168.1.101", "the configuration has a record with this name"} {
if !strings.Contains(body, want) {
t.Errorf("status lacks %q", want)
}
}
save() // unticked
if s, _ := loadSettings(sys.settingsPath()); s.dynamicLeases() {
t.Error("not switched off")
}
if _, ok := obj(obj(mustLoad(sys), "router"), "dns")["dynamicLeases"]; ok {
t.Error("the key stays after switching off")
}
}