Files
dns-updater/internal/config/config.go
T
unkinben 194080511c
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
records: add domain/IP-range allow+deny filtering
Adds a Filter (allow/deny CIDRs + allow/deny domain suffixes) applied to the
desired set before reconcile. Range rules match A/AAAA by value and PTR by the
address encoded in the reverse-DNS owner, so both the junk A record and its
reverse PTR are dropped together. Configured via -deny-ranges/-allow-ranges/
-deny-domains/-allow-domains (comma-separated) or DNS_UPDATER_* env.

Purpose: keep k8s pod/service CIDRs, LB VIP ranges and other internal addresses
out of the authoritative zones, and stop NOTAUTH updates for reverse zones the
server does not host (10.42.x, 198.18.200.x, etc.).
2026-07-18 01:59:29 +10:00

130 lines
4.8 KiB
Go

// Package config holds dns-updater's runtime configuration, sourced from flags
// with environment-variable fallbacks so it works as a systemd unit with an
// EnvironmentFile.
package config
import (
"flag"
"fmt"
"net"
"os"
"strings"
"time"
"git.unkin.net/unkin/dns-updater/internal/records"
)
// Config is the daemon configuration.
type Config struct {
Server string // authoritative write endpoint host[:port]
KeyFile string // BIND-style TSIG key file
RecordsFile string // desired records (zone|name|type|ttl|value)
StateFile string // last-applied state
ResyncEvery time.Duration // periodic reconcile safety net
Debounce time.Duration // coalesce bursts of watch events
Timeout time.Duration // per-update network timeout
WatchIface bool // subscribe to interface address changes
Oneshot bool // reconcile once and exit (no watching)
APIAddr string // status API address (unix socket path or host:port; empty disables)
LogLevel string // debug|info|warn|error
Filter records.Filter
}
const defaultPort = "53"
// Parse builds a Config from args (typically os.Args[1:]).
func Parse(args []string) (*Config, error) {
fs := flag.NewFlagSet("dns-updater", flag.ContinueOnError)
c := &Config{}
fs.StringVar(&c.Server, "server", env("DNS_UPDATER_SERVER", ""), "authoritative write endpoint host[:port]")
fs.StringVar(&c.KeyFile, "key-file", env("DNS_UPDATER_KEY_FILE", "/etc/dns-updater/key"), "BIND-style TSIG key file")
fs.StringVar(&c.RecordsFile, "records-file", env("DNS_UPDATER_RECORDS_FILE", "/var/lib/dns-updater/records"), "desired records file")
fs.StringVar(&c.StateFile, "state-file", env("DNS_UPDATER_STATE_FILE", "/var/lib/dns-updater/applied"), "last-applied state file")
fs.DurationVar(&c.ResyncEvery, "resync", envDur("DNS_UPDATER_RESYNC", 10*time.Minute), "periodic reconcile interval (0 disables)")
fs.DurationVar(&c.Debounce, "debounce", envDur("DNS_UPDATER_DEBOUNCE", 2*time.Second), "coalesce watch events for this long")
fs.DurationVar(&c.Timeout, "timeout", envDur("DNS_UPDATER_TIMEOUT", 10*time.Second), "per-update network timeout")
fs.BoolVar(&c.WatchIface, "watch-interfaces", envBool("DNS_UPDATER_WATCH_INTERFACES", true), "reconcile on interface address changes")
fs.BoolVar(&c.Oneshot, "oneshot", envBool("DNS_UPDATER_ONESHOT", false), "reconcile once and exit")
fs.StringVar(&c.APIAddr, "api", env("DNS_UPDATER_API", "/run/dns-updater/api.sock"), "status API address (unix path or host:port; empty disables)")
fs.StringVar(&c.LogLevel, "log-level", env("DNS_UPDATER_LOG_LEVEL", "info"), "log level: debug|info|warn|error")
denyRanges := fs.String("deny-ranges", env("DNS_UPDATER_DENY_RANGES", ""), "comma-separated CIDRs to never publish (A value / PTR address in range is dropped)")
allowRanges := fs.String("allow-ranges", env("DNS_UPDATER_ALLOW_RANGES", ""), "comma-separated CIDRs; if set, only addresses in these are published")
denyDomains := fs.String("deny-domains", env("DNS_UPDATER_DENY_DOMAINS", ""), "comma-separated domain suffixes to never publish")
allowDomains := fs.String("allow-domains", env("DNS_UPDATER_ALLOW_DOMAINS", ""), "comma-separated domain suffixes; if set, only these are published")
if err := fs.Parse(args); err != nil {
return nil, err
}
if c.Server == "" {
return nil, fmt.Errorf("server is required (-server or DNS_UPDATER_SERVER)")
}
c.Server = withPort(c.Server)
var err error
if c.Filter.DenyRanges, err = parseCIDRs(*denyRanges); err != nil {
return nil, fmt.Errorf("deny-ranges: %w", err)
}
if c.Filter.AllowRanges, err = parseCIDRs(*allowRanges); err != nil {
return nil, fmt.Errorf("allow-ranges: %w", err)
}
c.Filter.DenyDomains = splitList(*denyDomains)
c.Filter.AllowDomains = splitList(*allowDomains)
return c, nil
}
func splitList(s string) []string {
var out []string
for _, p := range strings.Split(s, ",") {
if p = strings.TrimSpace(p); p != "" {
out = append(out, p)
}
}
return out
}
func parseCIDRs(s string) ([]*net.IPNet, error) {
var out []*net.IPNet
for _, p := range splitList(s) {
_, n, err := net.ParseCIDR(p)
if err != nil {
return nil, fmt.Errorf("%q: %w", p, err)
}
out = append(out, n)
}
return out, nil
}
func withPort(s string) string {
for i := len(s) - 1; i >= 0; i-- {
if s[i] == ':' {
return s // already has a port
}
if s[i] == ']' {
break // IPv6 literal without port
}
}
return s + ":" + defaultPort
}
func env(k, def string) string {
if v, ok := os.LookupEnv(k); ok {
return v
}
return def
}
func envDur(k string, def time.Duration) time.Duration {
if v, ok := os.LookupEnv(k); ok {
if d, err := time.ParseDuration(v); err == nil {
return d
}
}
return def
}
func envBool(k string, def bool) bool {
if v, ok := os.LookupEnv(k); ok {
return v == "1" || v == "true" || v == "yes"
}
return def
}