// Package config loads he-dydns configuration from a TOML file. package config import ( "fmt" "os" "path/filepath" "strings" "github.com/BurntSushi/toml" ) const DefaultIntervalMinutes = 5 // RecordType identifies which DNS record family an entry updates. Hurricane // Electric's dynamic DNS API cannot infer this from the submitted address // alone, so it must be configured explicitly per entry. type RecordType string const ( RecordTypeA RecordType = "A" RecordTypeAAAA RecordType = "AAAA" ) // Entry describes a single Hurricane Electric dynamic DNS hostname to keep updated. type Entry struct { Hostname string `toml:"hostname"` Token string `toml:"token"` // RecordType selects whether this entry updates the A (IPv4) or AAAA // (IPv6) record. Defaults to A when omitted. RecordType RecordType `toml:"record_type"` } // Config is the top-level he-dydns configuration. type Config struct { // Interface, if set, restricts address discovery to this network // interface instead of guessing the public IP via an external service. Interface string `toml:"interface"` // IntervalMinutes controls how often updates run in daemon mode. IntervalMinutes int `toml:"interval_minutes"` Entries []Entry `toml:"entries"` } // DefaultPath returns $HOME/.config/he-dydns/config.toml. func DefaultPath() (string, error) { home, err := os.UserHomeDir() if err != nil { return "", fmt.Errorf("determining home directory: %w", err) } return filepath.Join(home, ".config", "he-dydns", "config.toml"), nil } // Load reads and validates the configuration file at path. func Load(path string) (*Config, error) { var cfg Config if _, err := toml.DecodeFile(path, &cfg); err != nil { return nil, fmt.Errorf("reading config %s: %w", path, err) } if len(cfg.Entries) == 0 { return nil, fmt.Errorf("config %s: no entries defined", path) } for i := range cfg.Entries { e := &cfg.Entries[i] if e.Hostname == "" { return nil, fmt.Errorf("config %s: entries[%d] missing hostname", path, i) } if e.Token == "" { return nil, fmt.Errorf("config %s: entries[%d] missing token", path, i) } switch RecordType(strings.ToUpper(string(e.RecordType))) { case "": e.RecordType = RecordTypeA case RecordTypeA: e.RecordType = RecordTypeA case RecordTypeAAAA: e.RecordType = RecordTypeAAAA default: return nil, fmt.Errorf("config %s: entries[%d] invalid record_type %q (must be \"A\" or \"AAAA\")", path, i, e.RecordType) } } if cfg.IntervalMinutes <= 0 { cfg.IntervalMinutes = DefaultIntervalMinutes } return &cfg, nil }