package config import ( "flag" "fmt" "github.com/BurntSushi/toml" ) type Config struct { Server ServerConfig `toml:"server"` Cache CacheConfig `toml:"cache"` Resolver ResolverConfig `toml:"resolver"` Blocklist BlocklistConfig `toml:"blocklist"` Admin AdminConfig `toml:"admin"` ACL ACLConfig `toml:"acl"` Log LogConfig `toml:"log"` TLS TLSConfig `toml:"tls"` } type ServerConfig struct { ListenUDP string `toml:"listen_udp"` ListenTCP string `toml:"listen_tcp"` ListenDOH string `toml:"listen_doh"` ListenDoT string `toml:"listen_dot"` } type TLSConfig struct { Cert string `toml:"cert"` Key string `toml:"key"` } type CacheConfig struct { MaxEntries int `toml:"max_entries"` DBPath string `toml:"db_path"` } type ResolverConfig struct { Mode string `toml:"mode"` Timeout string `toml:"timeout"` MaxDelegations int `toml:"max_delegations"` Forwarders []string `toml:"forwarders"` } type BlocklistConfig struct { Response string `toml:"response"` Files []string `toml:"files"` URLs []string `toml:"urls"` } type LogConfig struct { Level string `toml:"level"` } type CLIFlags struct { Config string LogLevel string ListenUDP string ListenTCP string ListenDOH string } type ACLConfig struct { Allow []string `toml:"allow"` RateLimitQPS int `toml:"rate_limit_qps"` RateLimitBurst int `toml:"rate_limit_burst"` } type AdminConfig struct { Listen string `toml:"listen"` } func ParseFlags() CLIFlags { var f CLIFlags flag.StringVar(&f.Config, "config", "linum.toml", "path to config file") flag.StringVar(&f.LogLevel, "loglevel", "", "log level (debug|info|warn|error)") flag.StringVar(&f.ListenUDP, "udp", "", "UDP listen address") flag.StringVar(&f.ListenTCP, "tcp", "", "TCP listen address") flag.StringVar(&f.ListenDOH, "doh", "", "DoH listen address") flag.Parse() return f } func Default() Config { return Config{ Server: ServerConfig{ ListenUDP: ":5353", ListenTCP: ":5353", ListenDOH: ":8443", }, Cache: CacheConfig{ MaxEntries: 100000, }, Resolver: ResolverConfig{ Mode: "recursive", Timeout: "2s", MaxDelegations: 30, }, Blocklist: BlocklistConfig{ Response: "zero_ip", }, Log: LogConfig{ Level: "info", }, Admin: AdminConfig{ Listen: "127.0.0.1:8080", }, ACL: ACLConfig{ Allow: []string{}, RateLimitQPS: 50, RateLimitBurst: 10, }, } } func LoadFile(path string) (Config, error) { var cfg Config _, err := toml.DecodeFile(path, &cfg) return cfg, err } func Merge(dst, src Config) Config { if src.Server.ListenUDP != "" { dst.Server.ListenUDP = src.Server.ListenUDP } if src.Server.ListenTCP != "" { dst.Server.ListenTCP = src.Server.ListenTCP } if src.Server.ListenDOH != "" { dst.Server.ListenDOH = src.Server.ListenDOH } if src.Cache.MaxEntries > 0 { dst.Cache.MaxEntries = src.Cache.MaxEntries } if src.Cache.DBPath != "" { dst.Cache.DBPath = src.Cache.DBPath } if src.Resolver.Timeout != "" { dst.Resolver.Timeout = src.Resolver.Timeout } if src.Resolver.MaxDelegations > 0 { dst.Resolver.MaxDelegations = src.Resolver.MaxDelegations } if src.Resolver.Mode != "" { dst.Resolver.Mode = src.Resolver.Mode } if len(src.Resolver.Forwarders) > 0 { dst.Resolver.Forwarders = src.Resolver.Forwarders } if src.Blocklist.Response != "" { dst.Blocklist.Response = src.Blocklist.Response } if len(src.Blocklist.Files) > 0 { dst.Blocklist.Files = src.Blocklist.Files } if len(src.Blocklist.URLs) > 0 { dst.Blocklist.URLs = src.Blocklist.URLs } if src.Log.Level != "" { dst.Log.Level = src.Log.Level } if src.ACL.Allow != nil { dst.ACL.Allow = src.ACL.Allow } if src.ACL.RateLimitQPS != 0 { dst.ACL.RateLimitQPS = src.ACL.RateLimitQPS } if src.ACL.RateLimitBurst != 0 { dst.ACL.RateLimitBurst = src.ACL.RateLimitBurst } if src.Admin.Listen != "" { dst.Admin.Listen = src.Admin.Listen } if src.Server.ListenDoT != "" { dst.Server.ListenDoT = src.Server.ListenDoT } if src.TLS.Cert != "" { dst.TLS.Cert = src.TLS.Cert } if src.TLS.Key != "" { dst.TLS.Key = src.TLS.Key } return dst } func (f CLIFlags) Apply(cfg Config) Config { if f.ListenUDP != "" { cfg.Server.ListenUDP = f.ListenUDP } if f.ListenTCP != "" { cfg.Server.ListenTCP = f.ListenTCP } if f.ListenDOH != "" { cfg.Server.ListenDOH = f.ListenDOH } if f.LogLevel != "" { cfg.Log.Level = f.LogLevel } return cfg } func (c Config) Validate() error { switch c.Blocklist.Response { case "zero_ip", "nxdomain", "": default: return fmt.Errorf("invalid blocklist response %q (want zero_ip or nxdomain)", c.Blocklist.Response) } switch c.ACL.Allow { default: } switch c.Resolver.Mode { case "recursive", "forward", "": // nothing happened lol default: return fmt.Errorf("invalid resolver mode %q (recursive or forward)", c.Resolver.Mode) } if c.Resolver.Mode == "forward" && len(c.Resolver.Forwarders) == 0 { return fmt.Errorf("resolver mode=forward requires at least one forwarder") } if c.Server.ListenDoT != "" && (c.TLS.Cert == "" || c.TLS.Key == "") { return fmt.Errorf("listen_dot requires tls.cert and tls.key") } return nil }