194080511c
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.).
130 lines
4.8 KiB
Go
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
|
|
}
|