feat(dns): cache reverse lookups with ttl

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-04 03:06:17 +00:00
parent 58509bba4d
commit b7f6912ef0
3 changed files with 68 additions and 46 deletions

5
go.mod
View file

@ -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

2
go.sum
View file

@ -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=

View file

@ -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 {