// dynamic domain no-name system service // // a simple dynamic dns record updating utility which fetches your // ip address from a source, and lets a provider know of the fresh // address. package main import ( "flag" "fmt" "log/slog" "os" "git.rnrd.eu/ddnnss/provider" "git.rnrd.eu/ddnnss/source" ) const version = "1.0.0" func main() { configPath := flag.String("config", "/etc/ddnnss.json", "config file") showVersion := flag.Bool("version", false, "print the version and exit") flag.Parse() if *showVersion { fmt.Println("ddnnss", version) return } baseLog := slog.New(slog.NewTextHandler(os.Stderr, nil)) baseLog.Info("ddnnss!", slog.String("version", version)) cfg, err := LoadConfig(*configPath) if err != nil { baseLog.Error("could not load config", slog.String("error", err.Error())) os.Exit(1) } providers, sources := createModules(baseLog, cfg) baseLog.Info("starting new cycle", slog.Int("records", len(cfg.Records))) failed := false for _, r := range cfg.Records { fqdn := r.Name + "." + r.Zone log := baseLog.With( slog.Group("record", slog.String("fqdn", fqdn), slog.String("type", string(r.Type)), ), ) sourceName, providerName := r.Source, r.Provider log.Info( "ensuring record", slog.String("provider", providerName), slog.String("source", sourceName), ) s, ok := sources[sourceName] if !ok || s == nil { log.Error( "skipping record, failed creating source", slog.String("source", sourceName), ) failed = true continue } p, ok := providers[providerName] if !ok || p == nil { log.Error( "skipping record, failed creating provider", slog.String("provider", providerName), ) failed = true continue } family := source.IPv4 if r.Type == provider.TypeAAAA { family = source.IPv6 } if !s.Supports(family) { log.Error( "skipping record, source cannot provide the needed address family", slog.String("source", sourceName), slog.String("family", string(family)), ) failed = true continue } addr, err := s.Lookup(family) if err != nil { log.Error( "failed record, address lookup failed", slog.String("source", sourceName), slog.String("error", err.Error()), ) failed = true continue } prev, changed, err := p.Ensure(r.Zone, r.Name, r.Type, addr) switch { case err != nil: log.Error("record update failed", slog.String("error", err.Error())) failed = true case changed && prev == "": log.Info("record created", slog.String("addr", addr.String())) case changed: log.Info("record updated", slog.String("from", prev), slog.String("to", addr.String())) default: log.Info("record unchanged", "addr", addr) } } baseLog.Info( "cycle finished!", slog.Int("records", len(cfg.Records)), slog.Bool("failed", failed), ) if failed { baseLog.Error("not all records were ensured successfully") os.Exit(1) } } func createModules(log *slog.Logger, cfg *Config) (map[string]provider.Provider, map[string]source.Source) { sources := map[string]source.Source{} providers := map[string]provider.Provider{} for name, c := range cfg.Sources { s, err := source.New(log, name, c) if err != nil { log.Error( "failed creating source", slog.String("name", name), slog.String("kind", string(c.Kind)), slog.String("error", err.Error()), ) sources[name] = nil continue } sources[name] = s } for name, c := range cfg.Providers { p, err := provider.New(log, name, c) if err != nil { log.Error( "failed creating provider", slog.String("name", name), slog.String("kind", string(c.Kind)), slog.String("error", err.Error()), ) providers[name] = nil continue } providers[name] = p } return providers, sources }