From b7f6912ef052da843542814ce6adb7491754dc51 Mon Sep 17 00:00:00 2001 From: Virgil Date: Sat, 4 Apr 2026 03:06:17 +0000 Subject: [PATCH] feat(dns): cache reverse lookups with ttl Co-Authored-By: Virgil --- go.mod | 5 ++- go.sum | 2 + service.go | 107 +++++++++++++++++++++++++++++++---------------------- 3 files changed, 68 insertions(+), 46 deletions(-) diff --git a/go.mod b/go.mod index 92f2952..96a16e9 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,10 @@ module dappco.re/go/dns go 1.22 -require github.com/miekg/dns v1.1.62 +require ( + github.com/miekg/dns v1.1.62 + github.com/patrickmn/go-cache v2.1.0+incompatible +) require ( golang.org/x/mod v0.18.0 // indirect diff --git a/go.sum b/go.sum index 95e8194..26556ce 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= diff --git a/service.go b/service.go index 5e573f5..dbf8aee 100644 --- a/service.go +++ b/service.go @@ -10,6 +10,8 @@ import ( "strings" "sync" "time" + + cache "github.com/patrickmn/go-cache" ) // DefaultTreeRootCheckInterval is the cadence used to re-check the HSD tree root @@ -83,16 +85,16 @@ type ReverseLookupResult struct { Names []string `json:"names"` } -// ReverseIndex maps one IP to the names that point at it. +// ReverseIndex stores one IP to the names that point at it in a TTL-backed cache. // -// index := buildReverseIndex(records) +// index := buildReverseIndex(records, 15*time.Second) // names, ok := index.Lookup("10.10.10.10") type ReverseIndex struct { - ipToNames map[string][]string + namesByIP *cache.Cache } func (index *ReverseIndex) Lookup(ip string) ([]string, bool) { - if index == nil || len(index.ipToNames) == 0 { + if index == nil || index.namesByIP == nil { return nil, false } @@ -101,12 +103,17 @@ func (index *ReverseIndex) Lookup(ip string) ([]string, bool) { return nil, false } - names, found := index.ipToNames[normalizedIP] - if !found || len(names) == 0 { + names, found := index.namesByIP.Get(normalizedIP) + if !found { return nil, false } - return append([]string(nil), names...), true + typedNames, ok := names.([]string) + if !ok || len(typedNames) == 0 { + return nil, false + } + + return append([]string(nil), typedNames...), true } // HealthResult is the typed payload returned by Health and dns.health. @@ -302,7 +309,7 @@ func NewService(options ServiceOptions) *Service { service := &Service{ records: cached, recordExpiry: make(map[string]time.Time, len(cached)), - reverseIndex: buildReverseIndex(cached), + reverseIndex: buildReverseIndex(cached, options.RecordTTL), treeRoot: treeRoot, zoneApex: computeZoneApex(cached), dnsPort: options.DNSPort, @@ -1048,7 +1055,7 @@ func (service *Service) pruneExpiredRecords() { } func (service *Service) refreshDerivedStateLocked() { - service.reverseIndex = buildReverseIndex(service.records) + service.reverseIndex = buildReverseIndex(service.records, service.recordTTL) service.treeRoot = computeTreeRoot(service.records) service.zoneApex = computeZoneApex(service.records) } @@ -1319,45 +1326,55 @@ func emptyResolveAllResult() ResolveAllResult { } } -func buildReverseIndex(records map[string]NameRecords) *ReverseIndex { - raw := map[string]map[string]struct{}{} - for name, record := range records { - for _, ip := range record.A { - normalized := normalizeIP(ip) - if normalized == "" { - continue - } - index := raw[normalized] - if index == nil { - index = map[string]struct{}{} - raw[normalized] = index - } - index[name] = struct{}{} - } - for _, ip := range record.AAAA { - normalized := normalizeIP(ip) - if normalized == "" { - continue - } - index := raw[normalized] - if index == nil { - index = map[string]struct{}{} - raw[normalized] = index - } - index[name] = struct{}{} - } +func buildReverseIndex(records map[string]NameRecords, ttl time.Duration) *ReverseIndex { + expiration := cache.NoExpiration + if ttl > 0 { + expiration = ttl } - reverseIndex := make(map[string][]string, len(raw)) - for ip, names := range raw { - unique := make([]string, 0, len(names)) - for name := range names { - unique = append(unique, name) - } - slices.Sort(unique) - reverseIndex[ip] = unique + namesByIP := cache.New(expiration, ttl) + for name, record := range records { + collectReverseName(namesByIP, name, record.A, expiration) + collectReverseName(namesByIP, name, record.AAAA, expiration) + } + + for ip, item := range namesByIP.Items() { + typedNames, ok := item.Object.([]string) + if !ok { + namesByIP.Delete(ip) + continue + } + namesByIP.Set(ip, normalizeRecordValues(typedNames), expiration) + } + + return &ReverseIndex{namesByIP: namesByIP} +} + +func collectReverseName(namesByIP *cache.Cache, name string, ips []string, expiration time.Duration) { + if namesByIP == nil || len(ips) == 0 { + return + } + + for _, ip := range ips { + normalized := normalizeIP(ip) + if normalized == "" { + continue + } + + current, found := namesByIP.Get(normalized) + if !found { + namesByIP.Set(normalized, []string{name}, expiration) + continue + } + + typedNames, ok := current.([]string) + if !ok { + namesByIP.Set(normalized, []string{name}, expiration) + continue + } + + namesByIP.Set(normalized, append(typedNames, name), expiration) } - return &ReverseIndex{ipToNames: reverseIndex} } func normalizeIP(ip string) string {