v0.29.1: lanresolver restarts dnsmasq on change (SIGHUP doesn't re-read config)
After a guest DHCP IP move, the split-horizon resolver kept serving the old IP: the drop-in (address=/domain/ip) updated but 'systemctl reload dnsmasq' (SIGHUP) does NOT re-read /etc/dnsmasq.d config — only /etc/hosts + cache. Changed reload() -> restartDnsmasq() so address= changes actually take effect. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -75,7 +75,7 @@ func sanitize(s string) string {
|
||||
return strings.Trim(s, "-.") // also strip leading/trailing dots (no "..", no path-ish names)
|
||||
}
|
||||
|
||||
// Manager renders + applies the resolver config and reloads dnsmasq. It uses the same fenced Runner as
|
||||
// Manager renders + applies the resolver config and restarts dnsmasq (config drop-ins need a restart, not SIGHUP). It uses the same fenced Runner as
|
||||
// the rest of the agent (direct as root on the demo; sudo + a narrow allowlist in the hardened model).
|
||||
type Manager struct {
|
||||
runner proxmox.Runner
|
||||
@@ -113,18 +113,18 @@ func (m *Manager) EnsureDnsmasq(ctx context.Context) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("write base config: %w", err)
|
||||
}
|
||||
// enable + start (idempotent); reload if the base changed.
|
||||
// enable + start (idempotent); restart if the base changed (SIGHUP wont re-read the config).
|
||||
if _, _, err := m.runner.Run(ctx, "systemctl", "enable", "--now", "dnsmasq"); err != nil {
|
||||
return fmt.Errorf("enable dnsmasq: %w", err)
|
||||
}
|
||||
if changed {
|
||||
return m.reload(ctx)
|
||||
return m.restartDnsmasq(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReconcileGuest discovers the guest's live IP + domain and applies the per-customer drop-in (writing
|
||||
// + reloading only on change). It tolerates the early-boot pre-lease window: an empty IP is a no-op
|
||||
// + restarting only on change). It tolerates the early-boot pre-lease window: an empty IP is a no-op
|
||||
// (logged), never a blank/zero `address=` record. Logs IP transitions.
|
||||
func (m *Manager) ReconcileGuest(ctx context.Context, vmid int, customerID string) error {
|
||||
ip, err := m.discoverGuestIP(ctx, vmid)
|
||||
@@ -162,7 +162,7 @@ func (m *Manager) ReconcileGuest(ctx context.Context, vmid int, customerID strin
|
||||
} else {
|
||||
m.logger.Info("lanresolver: applied split-horizon record", "vmid", vmid, "customer", customerID, "domain", domain, "ip", ip)
|
||||
}
|
||||
if err := m.reload(ctx); err != nil {
|
||||
if err := m.restartDnsmasq(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -172,13 +172,13 @@ func (m *Manager) ReconcileGuest(ctx context.Context, vmid int, customerID strin
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove deletes a customer's drop-in (decommission) and reloads.
|
||||
// Remove deletes a customer's drop-in (decommission) and restarts dnsmasq.
|
||||
func (m *Manager) Remove(ctx context.Context, customerID string) error {
|
||||
path := filepath.Join(DropinDir, DropinName(customerID))
|
||||
if _, _, err := m.runner.Run(ctx, "rm", "-f", path); err != nil {
|
||||
return fmt.Errorf("remove drop-in: %w", err)
|
||||
}
|
||||
return m.reload(ctx)
|
||||
return m.restartDnsmasq(ctx)
|
||||
}
|
||||
|
||||
// discoverGuestIP runs `pct exec <vmid> -- ip -4 -o addr show dev eth0` and parses the inet address.
|
||||
@@ -231,9 +231,16 @@ func parseDomain(s string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Manager) reload(ctx context.Context) error {
|
||||
if _, errOut, err := m.runner.Run(ctx, "systemctl", "reload", "dnsmasq"); err != nil {
|
||||
return fmt.Errorf("reload dnsmasq: %s: %w", strings.TrimSpace(string(errOut)), err)
|
||||
// restartDnsmasq applies a changed drop-in by RESTARTING dnsmasq — NOT `systemctl reload` (SIGHUP).
|
||||
// dnsmasq's SIGHUP only clears the cache + re-reads /etc/hosts & addn-hosts; it does NOT re-read the
|
||||
// configuration files in /etc/dnsmasq.d. Our split-horizon records are CONFIG directives
|
||||
// (`local=/<domain>/` + `address=/<domain>/<ip>`), so a reloaded dnsmasq keeps serving the IP it read
|
||||
// at startup — exactly the stale-IP bug seen after a guest's DHCP IP moved (the file updated, but
|
||||
// `reload` left the old `address=` live). A restart re-reads the drop-ins. dnsmasq restart is sub-second
|
||||
// and the records carry local-ttl 0, so downstream forwarders (Pi-hole) don't cache a stale answer.
|
||||
func (m *Manager) restartDnsmasq(ctx context.Context) error {
|
||||
if _, errOut, err := m.runner.Run(ctx, "systemctl", "restart", "dnsmasq"); err != nil {
|
||||
return fmt.Errorf("restart dnsmasq: %s: %w", strings.TrimSpace(string(errOut)), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user