Alejandro Mery
1 year ago
4 changed files with 408 additions and 8 deletions
@ -0,0 +1,347 @@ |
|||||||
|
package dns |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"errors" |
||||||
|
"net/netip" |
||||||
|
"sort" |
||||||
|
"strings" |
||||||
|
"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 |
||||||
|
} |
||||||
|
|
||||||
|
// GetRecords pulls all the address records on DNS for our domain
|
||||||
|
func (mgr *Manager) GetRecords(ctx context.Context) ([]SyncAddrRecord, error) { |
||||||
|
if mgr.p == nil { |
||||||
|
return nil, errors.New("dns provider not specified") |
||||||
|
} |
||||||
|
|
||||||
|
recs, err := mgr.p.GetRecords(ctx, mgr.domain) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return mgr.filteredRecords(recs) |
||||||
|
} |
||||||
|
|
||||||
|
// AsSyncAddr converts a A or AAAA [libdns.Record] into a [SyncAddr]
|
||||||
|
func (mgr *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 |
||||||
|
} |
||||||
|
|
||||||
|
// skip entries not containing our suffix
|
||||||
|
if mgr.suffix != "" { |
||||||
|
if !strings.HasSuffix(rr.Name, mgr.suffix) { |
||||||
|
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) filteredRecords(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.GetRecords(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 |
||||||
|
} |
Loading…
Reference in new issue