diff --git a/internal/pxe/dnsmasq.go b/internal/pxe/dnsmasq.go index 6cc8916..98e44a1 100644 --- a/internal/pxe/dnsmasq.go +++ b/internal/pxe/dnsmasq.go @@ -120,7 +120,10 @@ func (s *Supervisor) Start(ctx context.Context, hosts []model.Host) error { if err := os.MkdirAll(s.cfg.RuntimeDir, 0o755); err != nil { return fmt.Errorf("mkdir runtime: %w", err) } - if err := s.writeConf(hosts); err != nil { + if err := s.writeHosts(hosts); err != nil { + return err + } + if err := s.writeConf(); err != nil { return err } subCtx, cancel := context.WithCancel(ctx) @@ -152,14 +155,14 @@ func (s *Supervisor) Start(ctx context.Context, hosts []model.Host) error { return nil } -// Reload rewrites the conf with the latest host registry and sends -// SIGHUP. It will restart the subprocess if SIGHUP is unsupported -// (e.g. when running behind an OS that doesn't support it). +// Reload rewrites the dhcp-hosts allowlist with the latest host +// registry and SIGHUPs dnsmasq to pick it up. The main dnsmasq.conf +// is unchanged — it only references the hosts file by path. func (s *Supervisor) Reload(hosts []model.Host) error { if !s.cfg.Enabled { return nil } - if err := s.writeConf(hosts); err != nil { + if err := s.writeHosts(hosts); err != nil { return err } s.mu.Lock() @@ -201,7 +204,7 @@ func (s *Supervisor) Shutdown(timeout time.Duration) error { return nil } -func (s *Supervisor) writeConf(hosts []model.Host) error { +func (s *Supervisor) writeConf() error { tmpl, err := template.New("dnsmasq").Parse(dnsmasqTemplate) if err != nil { return err @@ -219,10 +222,9 @@ func (s *Supervisor) writeConf(hosts []model.Host) error { } data := struct { Cfg SupervisorConfig - Hosts []model.Host Network string Netmask string - }{s.cfg, hosts, ipnet.IP.String(), net.IP(ipnet.Mask).String()} + }{s.cfg, ipnet.IP.String(), net.IP(ipnet.Mask).String()} if err := tmpl.Execute(f, data); err != nil { _ = f.Close() return fmt.Errorf("render conf: %w", err) @@ -240,6 +242,32 @@ func (s *Supervisor) writeConf(hosts []model.Host) error { return nil } +// writeHosts renders the dhcp-hostsfile referenced by dnsmasq.conf. +// Each registered host contributes one line: +// +// ,set:known +// +// dnsmasq re-reads this file on SIGHUP — that's the whole point of +// keeping it separate from the main conf. +func (s *Supervisor) writeHosts(hosts []model.Host) error { + if err := os.MkdirAll(s.cfg.RuntimeDir, 0o755); err != nil { + return fmt.Errorf("mkdir runtime: %w", err) + } + path := filepath.Join(s.cfg.RuntimeDir, "dhcp-hosts") + tmp := path + ".new" + var b strings.Builder + for _, h := range hosts { + fmt.Fprintf(&b, "%s,set:known\n", h.MAC) + } + if err := os.WriteFile(tmp, []byte(b.String()), 0o644); err != nil { + return fmt.Errorf("write dhcp-hosts: %w", err) + } + if err := os.Rename(tmp, path); err != nil { + return fmt.Errorf("rename dhcp-hosts: %w", err) + } + return nil +} + // Exposed for the UI handlers to show operators what config is live. func (s *Supervisor) ConfPath() string { return filepath.Join(s.cfg.RuntimeDir, "dnsmasq.conf") @@ -275,11 +303,12 @@ no-resolv # CIDR — we split Subnet upstream in writeConf(). dhcp-range={{ .Network }},proxy,{{ .Netmask }} -# MAC allowlist: dnsmasq only answers DHCP for MACs with a dhcp-host= below. +# MAC allowlist: dnsmasq only answers DHCP for MACs tagged "known". +# The per-MAC dhcp-host= entries live in a separate file so SIGHUP +# can reload them — dnsmasq does NOT re-read dhcp-host= from the +# main conf on SIGHUP, only from dhcp-hostsfile=. dhcp-ignore=tag:!known -{{- range .Hosts }} -dhcp-host={{ .MAC }},set:known -{{- end }} +dhcp-hostsfile={{ .Cfg.RuntimeDir }}/dhcp-hosts # Keep runtime state inside RuntimeDir so the systemd sandbox # (ReadWritePaths=/var/lib/vetting ...) doesn't block writes to the