initial commit

This commit is contained in:
2026-08-16 21:18:45 -05:00
commit 1e05a01bcf
122 changed files with 29178 additions and 0 deletions
+122
View File
@@ -0,0 +1,122 @@
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",
}
// 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"
)
// 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))
}
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
}