package config import ( "fmt" "net" "net/netip" "net/url" "strconv" "strings" ) // Setting keys. Keeping them as constants means a typo is a compile error // rather than a silently ignored setting. const ( KeyDNSUDPListen = "dns.udp_listen" KeyDNSTCPListen = "dns.tcp_listen" KeyDNSRecursion = "dns.recursion_enabled" KeyDNSEDNSEnabled = "dns.edns_enabled" KeyDNSEDNSUDPSize = "dns.edns_udp_size" KeyDNSDefaultTTL = "dns.default_ttl" KeyDNSTCPIdle = "dns.tcp_idle_timeout_s" KeyDNSExposeVer = "dns.expose_version" KeyDNSMaxUDPSize = "dns.max_udp_response" KeyResolverUpstreams = "resolver.upstreams" KeyResolverTimeout = "resolver.timeout_ms" KeyResolverRetries = "resolver.retries" KeyResolverStrategy = "resolver.strategy" KeyResolverAllow = "resolver.allow_networks" KeyResolverDeny = "resolver.deny_networks" KeyResolverPreferV6 = "resolver.prefer_ipv6" KeyResolverDNSSEC = "resolver.dnssec_enabled" KeyResolverMaxConc = "resolver.max_concurrent" KeyCacheEnabled = "cache.enabled" KeyCacheMaxEntries = "cache.max_entries" KeyCacheMinTTL = "cache.min_ttl" KeyCacheMaxTTL = "cache.max_ttl" KeyCacheNegativeTTL = "cache.negative_ttl" KeyCacheServeStale = "cache.serve_stale" KeyCacheStaleTTL = "cache.stale_ttl" KeyCachePrefetch = "cache.prefetch_enabled" KeyCachePrefetchPct = "cache.prefetch_threshold_pct" KeyCacheCleanup = "cache.cleanup_interval_s" KeyQueryLogEnabled = "querylog.enabled" KeyQueryLogRetention = "querylog.retention_days" KeyQueryLogMaxRows = "querylog.max_rows" KeyQueryLogCleanup = "querylog.cleanup_interval_min" KeyQueryLogIgnoreNet = "querylog.ignore_networks" KeyQueryLogIgnoreDom = "querylog.ignore_domains" KeyRateLimitEnabled = "ratelimit.enabled" KeyRateLimitQPS = "ratelimit.qps" KeyRateLimitBurst = "ratelimit.burst" KeyRateLimitExempt = "ratelimit.exempt_networks" KeyHTTPListen = "http.listen" KeyHTTPBaseURL = "http.base_url" KeyHTTPTrustedProxy = "http.trusted_proxies" KeyHTTPMetrics = "http.metrics_enabled" KeyHTTPMetricsPublic = "http.metrics_public" KeyHTTPMaxUploadMB = "http.max_upload_mb" KeyHTTPRateLimit = "http.rate_limit_per_min" KeyBackupEnabled = "backup.enabled" KeyBackupDir = "backup.dir" KeyBackupInterval = "backup.interval_hours" KeyBackupRetention = "backup.retention" KeyLogLevel = "log.level" KeyLogFormat = "log.format" KeyAuditMaxRows = "log.audit_max_rows" KeyBlockPageEnabled = "blockpage.enabled" KeyBlockPageHTTPListen = "blockpage.http_listen" KeyBlockPageHTTPSListen = "blockpage.https_listen" KeyBlockPageHTML = "blockpage.html" ) // DNSSettings covers the listeners and protocol behaviour. type DNSSettings struct { UDPListen string `json:"udp_listen"` TCPListen string `json:"tcp_listen"` Recursion bool `json:"recursion_enabled"` EDNSEnabled bool `json:"edns_enabled"` EDNSUDPSize int `json:"edns_udp_size"` MaxUDPResponse int `json:"max_udp_response"` DefaultTTL uint32 `json:"default_ttl"` TCPIdleSeconds int `json:"tcp_idle_timeout_s"` ExposeVersion bool `json:"expose_version"` } // ResolverSettings covers upstream forwarding and recursion ACLs. type ResolverSettings struct { Upstreams []string `json:"upstreams"` TimeoutMS int `json:"timeout_ms"` Retries int `json:"retries"` Strategy string `json:"strategy"` AllowNetworks []string `json:"allow_networks"` DenyNetworks []string `json:"deny_networks"` PreferIPv6 bool `json:"prefer_ipv6"` DNSSEC bool `json:"dnssec_enabled"` MaxConcurrent int `json:"max_concurrent"` } // Server selection strategies. const ( StrategySequential = "sequential" StrategyRoundRobin = "round_robin" StrategyRandom = "random" StrategyFastest = "fastest" ) // CacheSettings covers the resolver cache. type CacheSettings struct { Enabled bool `json:"enabled"` MaxEntries int `json:"max_entries"` MinTTL int `json:"min_ttl"` MaxTTL int `json:"max_ttl"` NegativeTTL int `json:"negative_ttl"` ServeStale bool `json:"serve_stale"` StaleTTL int `json:"stale_ttl"` Prefetch bool `json:"prefetch_enabled"` PrefetchPercent int `json:"prefetch_threshold_pct"` CleanupSeconds int `json:"cleanup_interval_s"` } // QueryLogSettings covers DNS query logging and its retention. type QueryLogSettings struct { Enabled bool `json:"enabled"` RetentionDays int `json:"retention_days"` MaxRows int `json:"max_rows"` CleanupMinutes int `json:"cleanup_interval_min"` IgnoreNetworks []string `json:"ignore_networks"` IgnoreDomains []string `json:"ignore_domains"` } // RateLimitSettings covers per-client DNS rate limiting. type RateLimitSettings struct { Enabled bool `json:"enabled"` QPS int `json:"qps"` Burst int `json:"burst"` ExemptNetworks []string `json:"exempt_networks"` } // HTTPSettings covers the management interface. type HTTPSettings struct { Listen string `json:"listen"` BaseURL string `json:"base_url"` TrustedProxies []string `json:"trusted_proxies"` MetricsEnabled bool `json:"metrics_enabled"` MetricsPublic bool `json:"metrics_public"` MaxUploadMB int `json:"max_upload_mb"` RateLimitPerMin int `json:"rate_limit_per_min"` } // BackupSettings covers automatic database backups. type BackupSettings struct { Enabled bool `json:"enabled"` Directory string `json:"dir"` IntervalHours int `json:"interval_hours"` Retention int `json:"retention"` } // LoggingSettings covers application log output. type LoggingSettings struct { Level string `json:"level"` Format string `json:"format"` AuditMaxRows int `json:"audit_max_rows"` } // BlockPageSettings covers the HTTP/HTTPS server that answers sinkholed // traffic with a page explaining why the request was blocked. type BlockPageSettings struct { Enabled bool `json:"enabled"` HTTPListen string `json:"http_listen"` HTTPSListen string `json:"https_listen"` HTML string `json:"html"` } // Settings is the complete runtime configuration held in SQLite. type Settings struct { DNS DNSSettings `json:"dns"` Resolver ResolverSettings `json:"resolver"` Cache CacheSettings `json:"cache"` QueryLog QueryLogSettings `json:"query_log"` RateLimit RateLimitSettings `json:"rate_limit"` HTTP HTTPSettings `json:"http"` Backup BackupSettings `json:"backup"` Logging LoggingSettings `json:"logging"` BlockPage BlockPageSettings `json:"block_page"` } // DefaultSettings returns a safe, closed-by-default configuration. // // Recursion is enabled but the allow list contains only loopback and private // address space, so a fresh install is never an open resolver. func DefaultSettings() Settings { return Settings{ DNS: DNSSettings{ UDPListen: DefaultDNSAddr, TCPListen: DefaultDNSAddr, Recursion: true, EDNSEnabled: true, EDNSUDPSize: 1232, // conservative post-DNS-flag-day value MaxUDPResponse: 1232, DefaultTTL: 3600, TCPIdleSeconds: 8, ExposeVersion: false, }, Resolver: ResolverSettings{ Upstreams: []string{"1.1.1.1:53", "1.0.0.1:53", "9.9.9.9:53"}, TimeoutMS: 2000, Retries: 2, Strategy: StrategyFastest, AllowNetworks: DefaultPrivateNetworks(), DenyNetworks: nil, PreferIPv6: false, DNSSEC: true, MaxConcurrent: 256, }, Cache: CacheSettings{ Enabled: true, MaxEntries: 100_000, MinTTL: 5, MaxTTL: 86400, NegativeTTL: 900, ServeStale: true, StaleTTL: 3600, Prefetch: true, PrefetchPercent: 10, CleanupSeconds: 60, }, QueryLog: QueryLogSettings{ Enabled: true, RetentionDays: 7, MaxRows: 1_000_000, CleanupMinutes: 30, }, RateLimit: RateLimitSettings{ Enabled: true, QPS: 200, Burst: 400, ExemptNetworks: []string{"127.0.0.0/8", "::1/128"}, }, HTTP: HTTPSettings{ Listen: DefaultHTTPAddr, MetricsEnabled: true, MetricsPublic: false, MaxUploadMB: 64, RateLimitPerMin: 600, }, Backup: BackupSettings{ Enabled: false, Directory: "./data/backups", IntervalHours: 24, Retention: 7, }, Logging: LoggingSettings{ Level: "info", Format: "text", AuditMaxRows: 50_000, }, BlockPage: BlockPageSettings{ Enabled: false, HTTPListen: ":80", HTTPSListen: ":443", HTML: DefaultBlockPageHTML, }, } } // DefaultPrivateNetworks lists the RFC1918/RFC4193 ranges used as the initial // recursion ACL. func DefaultPrivateNetworks() []string { return []string{ "127.0.0.0/8", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "169.254.0.0/16", "::1/128", "fc00::/7", "fe80::/10", } } // LoadSettings overlays stored values on top of the defaults. Unparseable or // missing values fall back to the default for that field, so a corrupted row // can never prevent the server from starting. func LoadSettings(stored map[string]string) Settings { s := DefaultSettings() g := getter{stored} s.DNS.UDPListen = g.str(KeyDNSUDPListen, s.DNS.UDPListen) s.DNS.TCPListen = g.str(KeyDNSTCPListen, s.DNS.TCPListen) s.DNS.Recursion = g.boolean(KeyDNSRecursion, s.DNS.Recursion) s.DNS.EDNSEnabled = g.boolean(KeyDNSEDNSEnabled, s.DNS.EDNSEnabled) s.DNS.EDNSUDPSize = g.integer(KeyDNSEDNSUDPSize, s.DNS.EDNSUDPSize) s.DNS.MaxUDPResponse = g.integer(KeyDNSMaxUDPSize, s.DNS.MaxUDPResponse) s.DNS.DefaultTTL = uint32(g.integer(KeyDNSDefaultTTL, int(s.DNS.DefaultTTL))) s.DNS.TCPIdleSeconds = g.integer(KeyDNSTCPIdle, s.DNS.TCPIdleSeconds) s.DNS.ExposeVersion = g.boolean(KeyDNSExposeVer, s.DNS.ExposeVersion) s.Resolver.Upstreams = g.lines(KeyResolverUpstreams, s.Resolver.Upstreams) s.Resolver.TimeoutMS = g.integer(KeyResolverTimeout, s.Resolver.TimeoutMS) s.Resolver.Retries = g.integer(KeyResolverRetries, s.Resolver.Retries) s.Resolver.Strategy = g.str(KeyResolverStrategy, s.Resolver.Strategy) s.Resolver.AllowNetworks = g.lines(KeyResolverAllow, s.Resolver.AllowNetworks) s.Resolver.DenyNetworks = g.lines(KeyResolverDeny, s.Resolver.DenyNetworks) s.Resolver.PreferIPv6 = g.boolean(KeyResolverPreferV6, s.Resolver.PreferIPv6) s.Resolver.DNSSEC = g.boolean(KeyResolverDNSSEC, s.Resolver.DNSSEC) s.Resolver.MaxConcurrent = g.integer(KeyResolverMaxConc, s.Resolver.MaxConcurrent) s.Cache.Enabled = g.boolean(KeyCacheEnabled, s.Cache.Enabled) s.Cache.MaxEntries = g.integer(KeyCacheMaxEntries, s.Cache.MaxEntries) s.Cache.MinTTL = g.integer(KeyCacheMinTTL, s.Cache.MinTTL) s.Cache.MaxTTL = g.integer(KeyCacheMaxTTL, s.Cache.MaxTTL) s.Cache.NegativeTTL = g.integer(KeyCacheNegativeTTL, s.Cache.NegativeTTL) s.Cache.ServeStale = g.boolean(KeyCacheServeStale, s.Cache.ServeStale) s.Cache.StaleTTL = g.integer(KeyCacheStaleTTL, s.Cache.StaleTTL) s.Cache.Prefetch = g.boolean(KeyCachePrefetch, s.Cache.Prefetch) s.Cache.PrefetchPercent = g.integer(KeyCachePrefetchPct, s.Cache.PrefetchPercent) s.Cache.CleanupSeconds = g.integer(KeyCacheCleanup, s.Cache.CleanupSeconds) s.QueryLog.Enabled = g.boolean(KeyQueryLogEnabled, s.QueryLog.Enabled) s.QueryLog.RetentionDays = g.integer(KeyQueryLogRetention, s.QueryLog.RetentionDays) s.QueryLog.MaxRows = g.integer(KeyQueryLogMaxRows, s.QueryLog.MaxRows) s.QueryLog.CleanupMinutes = g.integer(KeyQueryLogCleanup, s.QueryLog.CleanupMinutes) s.QueryLog.IgnoreNetworks = g.lines(KeyQueryLogIgnoreNet, s.QueryLog.IgnoreNetworks) s.QueryLog.IgnoreDomains = g.lines(KeyQueryLogIgnoreDom, s.QueryLog.IgnoreDomains) s.RateLimit.Enabled = g.boolean(KeyRateLimitEnabled, s.RateLimit.Enabled) s.RateLimit.QPS = g.integer(KeyRateLimitQPS, s.RateLimit.QPS) s.RateLimit.Burst = g.integer(KeyRateLimitBurst, s.RateLimit.Burst) s.RateLimit.ExemptNetworks = g.lines(KeyRateLimitExempt, s.RateLimit.ExemptNetworks) s.HTTP.Listen = g.str(KeyHTTPListen, s.HTTP.Listen) s.HTTP.BaseURL = g.str(KeyHTTPBaseURL, s.HTTP.BaseURL) s.HTTP.TrustedProxies = g.lines(KeyHTTPTrustedProxy, s.HTTP.TrustedProxies) s.HTTP.MetricsEnabled = g.boolean(KeyHTTPMetrics, s.HTTP.MetricsEnabled) s.HTTP.MetricsPublic = g.boolean(KeyHTTPMetricsPublic, s.HTTP.MetricsPublic) s.HTTP.MaxUploadMB = g.integer(KeyHTTPMaxUploadMB, s.HTTP.MaxUploadMB) s.HTTP.RateLimitPerMin = g.integer(KeyHTTPRateLimit, s.HTTP.RateLimitPerMin) s.Backup.Enabled = g.boolean(KeyBackupEnabled, s.Backup.Enabled) s.Backup.Directory = g.str(KeyBackupDir, s.Backup.Directory) s.Backup.IntervalHours = g.integer(KeyBackupInterval, s.Backup.IntervalHours) s.Backup.Retention = g.integer(KeyBackupRetention, s.Backup.Retention) s.Logging.Level = g.str(KeyLogLevel, s.Logging.Level) s.Logging.Format = g.str(KeyLogFormat, s.Logging.Format) s.Logging.AuditMaxRows = g.integer(KeyAuditMaxRows, s.Logging.AuditMaxRows) s.BlockPage.Enabled = g.boolean(KeyBlockPageEnabled, s.BlockPage.Enabled) s.BlockPage.HTTPListen = g.str(KeyBlockPageHTTPListen, s.BlockPage.HTTPListen) s.BlockPage.HTTPSListen = g.str(KeyBlockPageHTTPSListen, s.BlockPage.HTTPSListen) s.BlockPage.HTML = g.str(KeyBlockPageHTML, s.BlockPage.HTML) s.Normalise() return s } // ToMap renders the settings back into their stored representation. func (s Settings) ToMap() map[string]string { return map[string]string{ KeyDNSUDPListen: s.DNS.UDPListen, KeyDNSTCPListen: s.DNS.TCPListen, KeyDNSRecursion: boolStr(s.DNS.Recursion), KeyDNSEDNSEnabled: boolStr(s.DNS.EDNSEnabled), KeyDNSEDNSUDPSize: itoa(s.DNS.EDNSUDPSize), KeyDNSMaxUDPSize: itoa(s.DNS.MaxUDPResponse), KeyDNSDefaultTTL: itoa(int(s.DNS.DefaultTTL)), KeyDNSTCPIdle: itoa(s.DNS.TCPIdleSeconds), KeyDNSExposeVer: boolStr(s.DNS.ExposeVersion), KeyResolverUpstreams: strings.Join(s.Resolver.Upstreams, "\n"), KeyResolverTimeout: itoa(s.Resolver.TimeoutMS), KeyResolverRetries: itoa(s.Resolver.Retries), KeyResolverStrategy: s.Resolver.Strategy, KeyResolverAllow: strings.Join(s.Resolver.AllowNetworks, "\n"), KeyResolverDeny: strings.Join(s.Resolver.DenyNetworks, "\n"), KeyResolverPreferV6: boolStr(s.Resolver.PreferIPv6), KeyResolverDNSSEC: boolStr(s.Resolver.DNSSEC), KeyResolverMaxConc: itoa(s.Resolver.MaxConcurrent), KeyCacheEnabled: boolStr(s.Cache.Enabled), KeyCacheMaxEntries: itoa(s.Cache.MaxEntries), KeyCacheMinTTL: itoa(s.Cache.MinTTL), KeyCacheMaxTTL: itoa(s.Cache.MaxTTL), KeyCacheNegativeTTL: itoa(s.Cache.NegativeTTL), KeyCacheServeStale: boolStr(s.Cache.ServeStale), KeyCacheStaleTTL: itoa(s.Cache.StaleTTL), KeyCachePrefetch: boolStr(s.Cache.Prefetch), KeyCachePrefetchPct: itoa(s.Cache.PrefetchPercent), KeyCacheCleanup: itoa(s.Cache.CleanupSeconds), KeyQueryLogEnabled: boolStr(s.QueryLog.Enabled), KeyQueryLogRetention: itoa(s.QueryLog.RetentionDays), KeyQueryLogMaxRows: itoa(s.QueryLog.MaxRows), KeyQueryLogCleanup: itoa(s.QueryLog.CleanupMinutes), KeyQueryLogIgnoreNet: strings.Join(s.QueryLog.IgnoreNetworks, "\n"), KeyQueryLogIgnoreDom: strings.Join(s.QueryLog.IgnoreDomains, "\n"), KeyRateLimitEnabled: boolStr(s.RateLimit.Enabled), KeyRateLimitQPS: itoa(s.RateLimit.QPS), KeyRateLimitBurst: itoa(s.RateLimit.Burst), KeyRateLimitExempt: strings.Join(s.RateLimit.ExemptNetworks, "\n"), KeyHTTPListen: s.HTTP.Listen, KeyHTTPBaseURL: s.HTTP.BaseURL, KeyHTTPTrustedProxy: strings.Join(s.HTTP.TrustedProxies, "\n"), KeyHTTPMetrics: boolStr(s.HTTP.MetricsEnabled), KeyHTTPMetricsPublic: boolStr(s.HTTP.MetricsPublic), KeyHTTPMaxUploadMB: itoa(s.HTTP.MaxUploadMB), KeyHTTPRateLimit: itoa(s.HTTP.RateLimitPerMin), KeyBackupEnabled: boolStr(s.Backup.Enabled), KeyBackupDir: s.Backup.Directory, KeyBackupInterval: itoa(s.Backup.IntervalHours), KeyBackupRetention: itoa(s.Backup.Retention), KeyLogLevel: s.Logging.Level, KeyLogFormat: s.Logging.Format, KeyAuditMaxRows: itoa(s.Logging.AuditMaxRows), KeyBlockPageEnabled: boolStr(s.BlockPage.Enabled), KeyBlockPageHTTPListen: s.BlockPage.HTTPListen, KeyBlockPageHTTPSListen: s.BlockPage.HTTPSListen, KeyBlockPageHTML: s.BlockPage.HTML, } } // Normalise clamps values into sane ranges. It never rejects: it is applied // after loading so that odd stored values degrade rather than break startup. func (s *Settings) Normalise() { s.DNS.EDNSUDPSize = clamp(s.DNS.EDNSUDPSize, 512, 65535) s.DNS.MaxUDPResponse = clamp(s.DNS.MaxUDPResponse, 512, 65535) s.DNS.TCPIdleSeconds = clamp(s.DNS.TCPIdleSeconds, 1, 120) if s.DNS.DefaultTTL == 0 { s.DNS.DefaultTTL = 3600 } s.Resolver.TimeoutMS = clamp(s.Resolver.TimeoutMS, 100, 30000) s.Resolver.Retries = clamp(s.Resolver.Retries, 0, 10) s.Resolver.MaxConcurrent = clamp(s.Resolver.MaxConcurrent, 1, 10000) switch s.Resolver.Strategy { case StrategySequential, StrategyRoundRobin, StrategyRandom, StrategyFastest: default: s.Resolver.Strategy = StrategyFastest } s.Resolver.Upstreams = normaliseUpstreams(s.Resolver.Upstreams) s.Cache.MaxEntries = clamp(s.Cache.MaxEntries, 0, 10_000_000) s.Cache.MinTTL = clamp(s.Cache.MinTTL, 0, 86400) s.Cache.MaxTTL = clamp(s.Cache.MaxTTL, 1, 604800) if s.Cache.MinTTL > s.Cache.MaxTTL { s.Cache.MinTTL = s.Cache.MaxTTL } s.Cache.NegativeTTL = clamp(s.Cache.NegativeTTL, 0, 86400) s.Cache.StaleTTL = clamp(s.Cache.StaleTTL, 0, 604800) s.Cache.PrefetchPercent = clamp(s.Cache.PrefetchPercent, 1, 90) s.Cache.CleanupSeconds = clamp(s.Cache.CleanupSeconds, 5, 3600) s.QueryLog.RetentionDays = clamp(s.QueryLog.RetentionDays, 0, 3650) s.QueryLog.MaxRows = clamp(s.QueryLog.MaxRows, 0, 100_000_000) s.QueryLog.CleanupMinutes = clamp(s.QueryLog.CleanupMinutes, 1, 1440) s.RateLimit.QPS = clamp(s.RateLimit.QPS, 1, 1_000_000) s.RateLimit.Burst = clamp(s.RateLimit.Burst, 1, 1_000_000) if s.RateLimit.Burst < s.RateLimit.QPS { s.RateLimit.Burst = s.RateLimit.QPS } s.HTTP.MaxUploadMB = clamp(s.HTTP.MaxUploadMB, 1, 4096) s.HTTP.RateLimitPerMin = clamp(s.HTTP.RateLimitPerMin, 10, 1_000_000) s.HTTP.BaseURL = strings.TrimRight(strings.TrimSpace(s.HTTP.BaseURL), "/") s.Backup.IntervalHours = clamp(s.Backup.IntervalHours, 1, 8760) s.Backup.Retention = clamp(s.Backup.Retention, 1, 1000) switch strings.ToLower(s.Logging.Level) { case "debug", "info", "warn", "error": s.Logging.Level = strings.ToLower(s.Logging.Level) default: s.Logging.Level = "info" } switch strings.ToLower(s.Logging.Format) { case "text", "json": s.Logging.Format = strings.ToLower(s.Logging.Format) default: s.Logging.Format = "text" } s.Logging.AuditMaxRows = clamp(s.Logging.AuditMaxRows, 100, 10_000_000) if strings.TrimSpace(s.BlockPage.HTML) == "" { s.BlockPage.HTML = DefaultBlockPageHTML } } // Validate reports configuration errors that should be shown to the operator // rather than silently corrected. func (s Settings) Validate() error { if err := validateListenAddr(s.DNS.UDPListen); err != nil { return fmt.Errorf("DNS UDP listen address: %w", err) } if err := validateListenAddr(s.DNS.TCPListen); err != nil { return fmt.Errorf("DNS TCP listen address: %w", err) } if err := validateListenAddr(s.HTTP.Listen); err != nil { return fmt.Errorf("HTTP listen address: %w", err) } if s.DNS.Recursion && len(s.Resolver.Upstreams) == 0 { return fmt.Errorf("recursion is enabled but no upstream resolvers are configured") } for _, u := range s.Resolver.Upstreams { if err := validateUpstream(u); err != nil { return fmt.Errorf("upstream resolver %q: %w", u, err) } } if s.DNS.Recursion && len(s.Resolver.AllowNetworks) == 0 { return fmt.Errorf("recursion is enabled but the allowed-networks list is empty, " + "which would deny every client; add at least one network") } for label, list := range map[string][]string{ "allowed recursion network": s.Resolver.AllowNetworks, "denied recursion network": s.Resolver.DenyNetworks, "rate limit exempt network": s.RateLimit.ExemptNetworks, "query log ignored network": s.QueryLog.IgnoreNetworks, "trusted proxy": s.HTTP.TrustedProxies, } { for _, c := range list { if _, err := ParseCIDROrIP(c); err != nil { return fmt.Errorf("%s %q: %w", label, c, err) } } } if s.HTTP.BaseURL != "" { if _, err := url.Parse(s.HTTP.BaseURL); err != nil { return fmt.Errorf("base URL %q is not a valid URL", s.HTTP.BaseURL) } } if s.Backup.Enabled && strings.TrimSpace(s.Backup.Directory) == "" { return fmt.Errorf("backups are enabled but no backup directory is set") } if s.BlockPage.Enabled { if err := validateListenAddr(s.BlockPage.HTTPListen); err != nil { return fmt.Errorf("block page HTTP listen address: %w", err) } if err := validateListenAddr(s.BlockPage.HTTPSListen); err != nil { return fmt.Errorf("block page HTTPS listen address: %w", err) } if s.BlockPage.HTTPListen == s.BlockPage.HTTPSListen { return fmt.Errorf("block page HTTP and HTTPS listen addresses must differ") } for _, other := range []string{s.HTTP.Listen, s.DNS.UDPListen, s.DNS.TCPListen} { if s.BlockPage.HTTPListen == other || s.BlockPage.HTTPSListen == other { return fmt.Errorf("block page listen address %q collides with another listener", other) } } } return nil } // ParseCIDROrIP accepts either a CIDR block or a bare address, returning a // prefix. A bare address becomes a host route (/32 or /128). func ParseCIDROrIP(s string) (netip.Prefix, error) { s = strings.TrimSpace(s) if s == "" { return netip.Prefix{}, fmt.Errorf("must not be empty") } if strings.Contains(s, "/") { p, err := netip.ParsePrefix(s) if err != nil { return netip.Prefix{}, fmt.Errorf("not a valid CIDR block") } return p.Masked(), nil } addr, err := netip.ParseAddr(s) if err != nil { return netip.Prefix{}, fmt.Errorf("not a valid IP address or CIDR block") } return netip.PrefixFrom(addr, addr.BitLen()), nil } // normaliseUpstreams trims entries and appends the default port where missing. func normaliseUpstreams(in []string) []string { var out []string seen := map[string]bool{} for _, u := range in { u = strings.TrimSpace(u) if u == "" || strings.HasPrefix(u, "#") { continue } if _, _, err := net.SplitHostPort(u); err != nil { // Bare IPv6 addresses need brackets before a port can be appended. if strings.Count(u, ":") >= 2 && !strings.HasPrefix(u, "[") { u = "[" + u + "]:53" } else { u = u + ":53" } } if seen[u] { continue } seen[u] = true out = append(out, u) } return out } func validateUpstream(u string) error { host, port, err := net.SplitHostPort(u) if err != nil { return fmt.Errorf("expected host:port") } if net.ParseIP(host) == nil { return fmt.Errorf("host must be a literal IP address, not a name " + "(resolving upstream names would require the resolver we are configuring)") } p, err := strconv.Atoi(port) if err != nil || p < 1 || p > 65535 { return fmt.Errorf("invalid port %q", port) } return nil } // --- small helpers ------------------------------------------------------ type getter struct{ m map[string]string } func (g getter) str(key, def string) string { if v, ok := g.m[key]; ok { if t := strings.TrimSpace(v); t != "" { return t } } return def } func (g getter) boolean(key string, def bool) bool { v, ok := g.m[key] if !ok { return def } b, err := strconv.ParseBool(strings.TrimSpace(v)) if err != nil { return def } return b } func (g getter) integer(key string, def int) int { v, ok := g.m[key] if !ok { return def } n, err := strconv.Atoi(strings.TrimSpace(v)) if err != nil { return def } return n } // lines splits a multi-line setting, dropping blanks and comments. An empty // stored value means "explicitly empty" and overrides the default. func (g getter) lines(key string, def []string) []string { v, ok := g.m[key] if !ok { return def } return SplitLines(v) } // SplitLines parses a textarea-style list: one entry per line, "#" comments // and blank lines removed. Commas are also accepted as separators. func SplitLines(v string) []string { var out []string for _, line := range strings.FieldsFunc(v, func(r rune) bool { return r == '\n' || r == '\r' || r == ',' }) { line = strings.TrimSpace(line) if i := strings.Index(line, "#"); i >= 0 { line = strings.TrimSpace(line[:i]) } if line == "" { continue } out = append(out, line) } return out } func boolStr(b bool) string { if b { return "true" } return "false" } func itoa(i int) string { return strconv.Itoa(i) } func clamp(v, lo, hi int) int { if v < lo { return lo } if v > hi { return hi } return v }