package dns import ( "context" "net/netip" "sort" "time" "darvaza.org/core" "darvaza.org/slog" "github.com/libdns/libdns" ) // SyncAddrRecord is similar to AddrRecord but include libdns.Record details // fetched from the Provider type SyncAddrRecord struct { Name string Addrs []SyncAddr } // SyncAddr extends netip.Addr with ID and TTL fetched from the Provider type SyncAddr struct { ID string Addr netip.Addr TTL time.Duration } // Export assembles a libdns.Record func (rec *SyncAddr) Export(name string) libdns.Record { return libdns.Record{ ID: rec.ID, Name: name, Type: core.IIf(rec.Addr.Is6(), "AAAA", "A"), TTL: time.Second, Value: rec.Addr.String(), } } // SortSyncAddrSlice sorts a slice of [SyncAddr] by its address func SortSyncAddrSlice(s []SyncAddr) []SyncAddr { sort.Slice(s, func(i, j int) bool { a1 := s[i].Addr a2 := s[j].Addr return a1.Less(a2) }) return s } // GetSyncRecords pulls all the address records on DNS for our domain func (mgr *Manager) GetSyncRecords(ctx context.Context) ([]SyncAddrRecord, error) { recs, err := mgr.GetRecords(ctx) if err != nil { return nil, err } return mgr.filteredSyncRecords(recs) } // AsSyncAddr converts a A or AAAA [libdns.Record] into a [SyncAddr] func (*Manager) AsSyncAddr(rr libdns.Record) (SyncAddr, bool, error) { var out SyncAddr var addr netip.Addr // skip non-address types if rr.Type != "A" && rr.Type != "AAAA" { return out, false, nil } err := addr.UnmarshalText([]byte(rr.Value)) if err != nil { // invalid address on A or AAAA record return out, false, err } out = SyncAddr{ ID: rr.ID, TTL: rr.TTL, Addr: addr, } return out, true, nil } func (mgr *Manager) filteredSyncRecords(recs []libdns.Record) ([]SyncAddrRecord, error) { // filter and convert cache := make(map[string][]SyncAddr) for _, rr := range recs { addr, ok, err := mgr.AsSyncAddr(rr) switch { case err != nil: // skip invalid addresses mgr.l.Error(). WithField("subsystem", "dns"). WithField(slog.ErrorFieldName, err). WithField("name", rr.Name). WithField("type", rr.Type). WithField("addr", rr.Value). Print() case ok: // store cache[rr.Name] = append(cache[rr.Name], addr) } } // prepare records out := make([]SyncAddrRecord, len(cache)) names := make([]string, 0, len(cache)) for name := range cache { names = append(names, name) } sort.Strings(names) for i, name := range names { addrs := cache[name] out[i] = SyncAddrRecord{ Name: name, Addrs: SortSyncAddrSlice(addrs), } } return out, nil } // Sync updates all the address records on DNS for our domain func (mgr *Manager) Sync(ctx context.Context) error { current, err := mgr.GetSyncRecords(ctx) if err != nil { return core.Wrap(err, "GetRecords") } goal := mgr.genAllAddrRecords() for _, p := range makeSyncMap(current, goal) { err := mgr.doSync(ctx, p.Name, p.Before, p.After) if err != nil { return err } } return nil } func (mgr *Manager) doSync(ctx context.Context, name string, before []SyncAddr, after []netip.Addr) error { // var err error for _, a := range after { before, err = mgr.doSyncUpdateOrInsert(ctx, name, a, before) if err != nil { return err } } for _, b := range before { err = mgr.doSyncRemove(ctx, name, b) if err != nil { return err } } return nil } func (mgr *Manager) doSyncUpdateOrInsert(ctx context.Context, name string, addr netip.Addr, addrs []SyncAddr) ([]SyncAddr, error) { // var err error i, ok := findSyncAddrSorted(addr, addrs) if ok { rec := addrs[i] addrs = append(addrs[:i], addrs[i+1:]...) err = mgr.doSyncUpdate(ctx, name, addr, rec) } else { err = mgr.doSyncInsert(ctx, name, addr) } return addrs, err } func (mgr *Manager) doSyncUpdate(ctx context.Context, name string, addr netip.Addr, rec SyncAddr) error { // var log slog.Logger var msg string var err error if rec.TTL != time.Second { // amend TTL // TODO: batch updates _, err = mgr.p.SetRecords(ctx, mgr.domain, []libdns.Record{ rec.Export(name), }) if err == nil { log = mgr.l.Info() msg = "Updated" } else { log = mgr.l.Error(). WithField(slog.ErrorFieldName, err) msg = "Failed" } } else { log = mgr.l.Info() msg = "OK" } log. WithField("subsystem", "dns"). WithField("name", name). WithField("addr", addr). Print(msg) return err } func (mgr *Manager) doSyncInsert(ctx context.Context, name string, addr netip.Addr) error { // var log slog.Logger var msg string rec := libdns.Record{ Name: name, Type: core.IIf(addr.Is6(), "AAAA", "A"), TTL: time.Second, Value: addr.String(), } _, err := mgr.p.AppendRecords(ctx, mgr.domain, []libdns.Record{ rec, }) if err != nil { log = mgr.l.Error(). WithField(slog.ErrorFieldName, err) msg = "Failed to Add" } else { log = mgr.l.Info() msg = "Added" } log. WithField("subsystem", "dns"). WithField("name", name). WithField("addr", addr). Print(msg) return err } func (mgr *Manager) doSyncRemove(ctx context.Context, name string, rec SyncAddr) error { // var log slog.Logger var msg string // TODO: batch deletes _, err := mgr.p.DeleteRecords(ctx, mgr.domain, []libdns.Record{ rec.Export(name), }) if err != nil { log = mgr.l.Error(). WithField(slog.ErrorFieldName, err) msg = "Failed to Delete" } else { log = mgr.l.Warn() msg = "Deleted" } log. WithField("subsystem", "dns"). WithField("name", name). WithField("addr", rec.Addr). Print(msg) return err } func findSyncAddrSorted(target netip.Addr, addrs []SyncAddr) (int, bool) { for i, a := range addrs { switch target.Compare(a.Addr) { case 0: // match return i, true case -1: // miss return -1, false default: // next } } return -1, false } type syncMapEntry struct { Name string Before []SyncAddr After []netip.Addr } func makeSyncMap(current []SyncAddrRecord, goal []AddrRecord) map[string]syncMapEntry { // data := make(map[string]syncMapEntry) for _, cur := range current { me, ok := data[cur.Name] if !ok { me = syncMapEntry{ Name: cur.Name, } } me.Before = append(me.Before, cur.Addrs...) data[cur.Name] = me } for _, rr := range goal { me, ok := data[rr.Name] if !ok { me = syncMapEntry{ Name: rr.Name, } } me.After = append(me.After, rr.Addr...) data[rr.Name] = me } return data }