pxe: move dhcp-host allowlist into a SIGHUP-reloadable file
CI / Lint + build + test (push) Successful in 1m38s
Release / release (push) Successful in 2m25s

dnsmasq's SIGHUP re-reads /etc/ethers and any --dhcp-hostsfile= paths,
but NOT dhcp-host= lines from the main conf. Reload() was faithfully
rewriting dnsmasq.conf with the new MAC, sending SIGHUP, and then
dnsmasq kept serving its startup view — so a freshly-registered host
still showed up as "proxy-ignored, tags: eth0" with no "known" tag.

Split the allowlist into ${RuntimeDir}/dhcp-hosts, referenced from the
main conf via dhcp-hostsfile=. writeConf() is static-ish now; Reload
just rewrites the hosts file and SIGHUPs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-18 12:41:27 -04:00
parent bce6e08524
commit 2c440fce8a
+41 -12
View File
@@ -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:
//
// <mac>,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