package app import ( "context" "fmt" "sort" "strings" "github.com/owen/vibedns/internal/auditlog" "github.com/owen/vibedns/internal/config" ) // RestartRequired lists the settings that only take effect after a restart, // because they control a bound socket. var RestartRequired = map[string]string{ config.KeyDNSUDPListen: "DNS UDP listen address", config.KeyDNSTCPListen: "DNS TCP listen address", config.KeyHTTPListen: "Management HTTP listen address", config.KeyBlockPageEnabled: "Block page enabled state", config.KeyBlockPageHTTPListen: "Block page HTTP listen address", config.KeyBlockPageHTTPSListen: "Block page HTTPS listen address", } // SettingsGroup names a page of the settings interface. type SettingsGroup string // Settings pages. const ( GroupDNS SettingsGroup = "dns" GroupResolver SettingsGroup = "resolver" GroupCache SettingsGroup = "cache" GroupLogging SettingsGroup = "logging" GroupHTTP SettingsGroup = "http" GroupBackup SettingsGroup = "backup" GroupRateLimit SettingsGroup = "ratelimit" GroupBlockPage SettingsGroup = "blockpage" ) // SaveSettings validates and persists a complete settings object. // // Validation runs against the merged result rather than the submitted fields, // so a change that would leave the server in an unusable state — recursion on // with no upstreams, or an empty ACL — is rejected before it is stored. func (a *App) SaveSettings(ctx context.Context, actor auditlog.Actor, group SettingsGroup, next config.Settings) error { next.Normalise() if err := next.Validate(); err != nil { return Invalid("%s", err.Error()) } current := a.Settings() changed := diffSettings(current.ToMap(), next.ToMap()) if len(changed) == 0 { return nil } // Only the keys belonging to this group are written, so two administrators // editing different pages cannot overwrite each other's work. toWrite := map[string]string{} full := next.ToMap() for _, k := range changed { toWrite[k] = full[k] } if err := a.DB.SetSettings(ctx, toWrite); err != nil { return Internal(err, "The settings could not be saved.") } a.Audit.Record(ctx, actor, "settings.update", auditlog.ObjectSettings, string(group), string(group), auditlog.Changes("keys", strings.Join(changed, " "))) if err := a.Runtime.Reload(ctx); err != nil { return Internal(err, "The settings were saved but could not be applied. Restart the server.") } return nil } // diffSettings returns the keys whose values differ. func diffSettings(before, after map[string]string) []string { var changed []string for k, v := range after { if before[k] != v { changed = append(changed, k) } } sort.Strings(changed) return changed } // PendingRestart reports which changed settings need a restart to take effect. func (a *App) PendingRestart(ctx context.Context) []string { current := a.Settings() udp, tcp := a.DNS.ListenAddrs() var pending []string if current.DNS.UDPListen != udp { pending = append(pending, fmt.Sprintf("DNS UDP address (listening on %s, configured as %s)", udp, current.DNS.UDPListen)) } if current.DNS.TCPListen != tcp { pending = append(pending, fmt.Sprintf("DNS TCP address (listening on %s, configured as %s)", tcp, current.DNS.TCPListen)) } bpHTTP, bpHTTPS := a.BlockPage.ListenAddrs() bpRunning := a.BlockPage.Running() if current.BlockPage.Enabled != bpRunning { pending = append(pending, "Block page enabled state has changed") } else if bpRunning && (current.BlockPage.HTTPListen != bpHTTP || current.BlockPage.HTTPSListen != bpHTTPS) { pending = append(pending, fmt.Sprintf( "Block page address (listening on %s/%s, configured as %s/%s)", bpHTTP, bpHTTPS, current.BlockPage.HTTPListen, current.BlockPage.HTTPSListen)) } return pending } // TestUpstream probes one upstream resolver on demand. func (a *App) TestUpstream(ctx context.Context, addr, qname string) (string, error) { s := a.Settings() if strings.TrimSpace(qname) == "" { qname = "example.com" } upstreams := config.SplitLines(addr) if len(upstreams) == 0 { return "", Invalid("Enter an upstream resolver address.") } target := upstreams[0] if !strings.Contains(target, ":") { target += ":53" } rtt, rcode, err := resolverCheck(ctx, target, qname, s) if err != nil { return "", Invalid("%s", err.Error()) } return fmt.Sprintf("%s answered %s in %.0f ms", target, rcode, float64(rtt.Microseconds())/1000), nil }