From 9c6b2f4bc1aa41728d314ca00884029a78d1338e Mon Sep 17 00:00:00 2001 From: Virgil Date: Fri, 3 Apr 2026 19:49:08 +0000 Subject: [PATCH] feat(dns): add reverse dns index and lookup Co-Authored-By: Virgil --- service.go | 77 ++++++++++++++++++++++++++++++++++++++++++++++--- service_test.go | 48 ++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 4 deletions(-) diff --git a/service.go b/service.go index b0868b0..3011853 100644 --- a/service.go +++ b/service.go @@ -2,6 +2,7 @@ package dns import ( "fmt" + "net" "slices" "strings" "sync" @@ -22,8 +23,9 @@ type ResolveAllResult struct { } type Service struct { - mu sync.RWMutex - records map[string]NameRecords + mu sync.RWMutex + records map[string]NameRecords + reverseIndex map[string][]string } type ServiceOptions struct { @@ -36,7 +38,8 @@ func NewService(options ServiceOptions) *Service { cached[normalizeName(name)] = record } return &Service{ - records: cached, + records: cached, + reverseIndex: buildReverseIndex(cached), } } @@ -44,12 +47,14 @@ func (service *Service) SetRecord(name string, record NameRecords) { service.mu.Lock() defer service.mu.Unlock() service.records[normalizeName(name)] = record + service.reverseIndex = buildReverseIndex(service.records) } func (service *Service) RemoveRecord(name string) { service.mu.Lock() defer service.mu.Unlock() delete(service.records, normalizeName(name)) + service.reverseIndex = buildReverseIndex(service.records) } func (service *Service) Resolve(name string) (ResolveAllResult, bool) { @@ -68,6 +73,22 @@ func (service *Service) ResolveTXT(name string) ([]string, bool) { return append([]string(nil), record.TXT...), true } +func (service *Service) ResolveReverse(ip string) ([]string, bool) { + service.mu.RLock() + defer service.mu.RUnlock() + + normalizedIP := normalizeIP(ip) + if normalizedIP == "" { + return nil, false + } + + names, ok := service.reverseIndex[normalizedIP] + if !ok { + return nil, false + } + return append([]string(nil), names...), true +} + func (service *Service) ResolveAll(name string) (ResolveAllResult, bool) { record, ok := service.findRecord(name) if !ok { @@ -108,6 +129,55 @@ func resolveResult(record NameRecords) ResolveAllResult { } } +func buildReverseIndex(records map[string]NameRecords) map[string][]string { + 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{}{} + } + } + + 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 + } + return reverseIndex +} + +func normalizeIP(ip string) string { + parsed := net.ParseIP(strings.TrimSpace(ip)) + if parsed == nil { + return "" + } + return parsed.String() +} + func findWildcardMatch(name string, records map[string]NameRecords) (NameRecords, bool) { bestMatch := "" for candidate := range records { @@ -183,4 +253,3 @@ func MergeRecords(values ...[]string) []string { slices.Sort(unique) return unique } - diff --git a/service_test.go b/service_test.go index a82ae6c..334ca4c 100644 --- a/service_test.go +++ b/service_test.go @@ -62,3 +62,51 @@ func TestServiceResolveTXTUsesWildcard(t *testing.T) { } } +func TestServiceResolveReverseUsesARecords(t *testing.T) { + service := NewService(ServiceOptions{ + Records: map[string]NameRecords{ + "gateway.charon.lthn": { + A: []string{"10.10.10.10"}, + }, + }, + }) + + result, ok := service.ResolveReverse("10.10.10.10") + if !ok { + t.Fatal("expected reverse record to resolve") + } + if len(result) != 1 || result[0] != "gateway.charon.lthn" { + t.Fatalf("unexpected reverse result: %#v", result) + } +} + +func TestServiceResolveReverseFallsBackToFalseWhenUnknown(t *testing.T) { + service := NewService(ServiceOptions{ + Records: map[string]NameRecords{ + "gateway.charon.lthn": { + A: []string{"10.10.10.10"}, + }, + }, + }) + + if _, ok := service.ResolveReverse("10.10.10.11"); ok { + t.Fatal("expected no reverse match for unknown IP") + } +} + +func TestServiceResolveReverseUsesSetAndRemove(t *testing.T) { + service := NewService(ServiceOptions{}) + service.SetRecord("gateway.charon.lthn", NameRecords{ + AAAA: []string{"2600:1f1c:7f0:4f01:0000:0000:0000:0001"}, + }) + + result, ok := service.ResolveReverse("2600:1f1c:7f0:4f01::1") + if !ok || len(result) != 1 || result[0] != "gateway.charon.lthn" { + t.Fatalf("expected newly set reverse record, got %#v", result) + } + + service.RemoveRecord("gateway.charon.lthn") + if _, ok := service.ResolveReverse("2600:1f1c:7f0:4f01::1"); ok { + t.Fatal("expected removed reverse record to disappear") + } +} -- 2.45.3