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 reloads 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 := reloadDnsmasq(); err != nil { return fmt.Errorf("%s written, but reload 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 } func reloadDnsmasq() error { out, err := exec.Command("systemctl", "reload", "dnsmasq").CombinedOutput() if err != nil { return fmt.Errorf("%s", strings.TrimSpace(string(out))) } return nil }