Files
router/tools/ipadm/dnsmasq.go
T
mikeandClaude Sonnet 5 c135f205fa Expand LAN to 10.0.0.0/22 with static/reserved/dynamic/spare zones
Splits the LAN into four /24 zones: 10.0.0.0/24 static (no DHCP, DNS
only), 10.0.1.0/24 fixed DHCP reservations by MAC, 10.0.2.0/24 dynamic
DHCP pool, 10.0.3.0/24 spare/unused. ipadm now derives the required
subnet from whether a host has a MAC, auto-assigns free IPs, and
auto-migrates a host's IP when its MAC is added/removed. Migrated the
existing archerc80 reservation from 10.0.0.2 to 10.0.1.2.

Also fixes a latent bug found while doing this: dnsmasq's SIGHUP
(`systemctl reload`) only re-reads /etc/hosts, not the conf-dir files
ipadm writes to, so config changes were silently not applied on
reload. ipadm now restarts dnsmasq instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 08:57:58 +02:00

91 lines
2.7 KiB
Go

package main
import (
"fmt"
"os"
"os/exec"
"sort"
"strings"
)
// renderDnsmasqConfig builds the content of the generated dnsmasq include
// file from the current host list. Every host gets a host-record (forward +
// reverse DNS). Hosts with a MAC additionally get a dhcp-host static lease.
func renderDnsmasqConfig(hosts []Host) string {
sorted := make([]Host, len(hosts))
copy(sorted, hosts)
sort.Slice(sorted, func(i, j int) bool {
return strings.ToLower(sorted[i].Name) < strings.ToLower(sorted[j].Name)
})
var b strings.Builder
b.WriteString("# Generated by ipadm -u — DO NOT EDIT MANUALLY.\n")
b.WriteString("# Edit hosts with `ipadm` and re-run `ipadm -u` to regenerate this file.\n\n")
for _, h := range sorted {
if h.Comment != "" {
fmt.Fprintf(&b, "# %s: %s\n", h.Name, h.Comment)
}
fmt.Fprintf(&b, "host-record=%s,%s\n", h.Name, h.IP)
if h.MAC != "" {
fmt.Fprintf(&b, "dhcp-host=%s,%s,%s,infinite\n", h.MAC, h.IP, h.Name)
}
b.WriteString("\n")
}
return b.String()
}
// applyDnsmasqConfig writes the generated config to path, validates the full
// dnsmasq configuration, and on success restarts the dnsmasq service. On
// validation failure the previous file content is restored and the service
// is left untouched.
func applyDnsmasqConfig(path string, content string) error {
var backup []byte
hadFile := false
if b, err := os.ReadFile(path); err == nil {
backup = b
hadFile = true
} else if !os.IsNotExist(err) {
return fmt.Errorf("cannot read existing %s: %w", path, err)
}
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
return fmt.Errorf("cannot write %s: %w", path, err)
}
if err := runDnsmasqTest(); err != nil {
// roll back so a bad generated file never lingers
if hadFile {
os.WriteFile(path, backup, 0644)
} else {
os.Remove(path)
}
return fmt.Errorf("dnsmasq config invalid, change rolled back: %w", err)
}
if err := restartDnsmasq(); err != nil {
return fmt.Errorf("%s written, but restart failed: %w", path, err)
}
return nil
}
func runDnsmasqTest() error {
out, err := exec.Command("dnsmasq", "--test").CombinedOutput()
if err != nil {
return fmt.Errorf("%s", strings.TrimSpace(string(out)))
}
return nil
}
// restartDnsmasq does a full restart rather than `systemctl reload`
// (SIGHUP): dnsmasq's SIGHUP handler only re-reads /etc/hosts and a few
// specific hosts-file options, not the conf-dir files this generated
// hosts.conf lives in, so a reload would silently keep serving the old
// host-record/dhcp-host entries.
func restartDnsmasq() error {
out, err := exec.Command("systemctl", "restart", "dnsmasq").CombinedOutput()
if err != nil {
return fmt.Errorf("%s", strings.TrimSpace(string(out)))
}
return nil
}