// 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 }