From 9919c70ed1aba44b05327bfde61ab59887271e62 Mon Sep 17 00:00:00 2001 From: Virgil Date: Fri, 3 Apr 2026 19:54:47 +0000 Subject: [PATCH] feat(dns): add fallback discoverer for dns.discover Co-Authored-By: Virgil --- service.go | 55 ++++++++++++++++++++++++---------- service_test.go | 78 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 16 deletions(-) diff --git a/service.go b/service.go index d47dbee..0d41840 100644 --- a/service.go +++ b/service.go @@ -11,10 +11,10 @@ import ( ) type NameRecords struct { - A []string `json:"a"` - AAAA []string `json:"aaaa"` - TXT []string `json:"txt"` - NS []string `json:"ns"` + A []string `json:"a"` + AAAA []string `json:"aaaa"` + TXT []string `json:"txt"` + NS []string `json:"ns"` } type ResolveAllResult struct { @@ -29,16 +29,18 @@ type ResolveAddressResult struct { } type Service struct { - mu sync.RWMutex - records map[string]NameRecords - reverseIndex map[string][]string - treeRoot string - discoverer func() (map[string]NameRecords, error) + mu sync.RWMutex + records map[string]NameRecords + reverseIndex map[string][]string + treeRoot string + discoverer func() (map[string]NameRecords, error) + fallbackDiscoverer func() (map[string]NameRecords, error) } type ServiceOptions struct { - Records map[string]NameRecords - Discoverer func() (map[string]NameRecords, error) + Records map[string]NameRecords + Discoverer func() (map[string]NameRecords, error) + FallbackDiscoverer func() (map[string]NameRecords, error) } func NewService(options ServiceOptions) *Service { @@ -48,24 +50,46 @@ func NewService(options ServiceOptions) *Service { } treeRoot := computeTreeRoot(cached) return &Service{ - records: cached, - reverseIndex: buildReverseIndex(cached), - treeRoot: treeRoot, - discoverer: options.Discoverer, + records: cached, + reverseIndex: buildReverseIndex(cached), + treeRoot: treeRoot, + discoverer: options.Discoverer, + fallbackDiscoverer: options.FallbackDiscoverer, } } func (service *Service) Discover() error { discoverer := service.discoverer + fallback := service.fallbackDiscoverer if discoverer == nil { + if fallback == nil { + return nil + } + discovered, err := fallback() + if err != nil { + return err + } + service.replaceRecords(discovered) return nil } discovered, err := discoverer() if err != nil { + if fallback == nil { + return err + } + discovered, err = fallback() + if err != nil { + return err + } + service.replaceRecords(discovered) return err } + service.replaceRecords(discovered) + return nil +} +func (service *Service) replaceRecords(discovered map[string]NameRecords) { cached := make(map[string]NameRecords, len(discovered)) for name, record := range discovered { normalizedName := normalizeName(name) @@ -80,7 +104,6 @@ func (service *Service) Discover() error { service.records = cached service.reverseIndex = buildReverseIndex(service.records) service.treeRoot = computeTreeRoot(service.records) - return nil } func (service *Service) SetRecord(name string, record NameRecords) { diff --git a/service_test.go b/service_test.go index 5660aaf..90a6b80 100644 --- a/service_test.go +++ b/service_test.go @@ -1,6 +1,7 @@ package dns import ( + "errors" "strings" "testing" "time" @@ -254,6 +255,83 @@ func TestServiceDiscoverReplacesRecordsFromDiscoverer(t *testing.T) { } } +func TestServiceDiscoverFallsBackWhenPrimaryDiscovererFails(t *testing.T) { + primaryCalled := false + fallbackCalled := false + + service := NewService(ServiceOptions{ + Records: map[string]NameRecords{ + "legacy.charon.lthn": { + A: []string{"10.11.11.11"}, + }, + }, + Discoverer: func() (map[string]NameRecords, error) { + primaryCalled = true + return nil, errors.New("chain service unavailable") + }, + FallbackDiscoverer: func() (map[string]NameRecords, error) { + fallbackCalled = true + return map[string]NameRecords{ + "gateway.charon.lthn": { + A: []string{"10.10.10.10"}, + }, + }, nil + }, + }) + + if err := service.Discover(); err != nil { + t.Fatalf("expected fallback discovery to succeed: %v", err) + } + if !primaryCalled { + t.Fatal("expected primary discoverer to be attempted") + } + if !fallbackCalled { + t.Fatal("expected fallback discoverer to run after primary failure") + } + + result, ok := service.Resolve("gateway.charon.lthn") + if !ok { + t.Fatal("expected fallback record to resolve") + } + if len(result.A) != 1 || result.A[0] != "10.10.10.10" { + t.Fatalf("unexpected fallback resolve result: %#v", result.A) + } + if _, ok := service.Resolve("legacy.charon.lthn"); ok { + t.Fatal("expected legacy record to be replaced by fallback discovery") + } +} + +func TestServiceDiscoverUsesFallbackOnlyWhenPrimaryMissing(t *testing.T) { + fallbackCalled := false + + service := NewService(ServiceOptions{ + Records: map[string]NameRecords{ + "legacy.charon.lthn": { + A: []string{"10.11.11.11"}, + }, + }, + FallbackDiscoverer: func() (map[string]NameRecords, error) { + fallbackCalled = true + return map[string]NameRecords{ + "gateway.charon.lthn": { + A: []string{"10.10.10.20"}, + }, + }, nil + }, + }) + + if err := service.Discover(); err != nil { + t.Fatalf("expected fallback discovery to run: %v", err) + } + if !fallbackCalled { + t.Fatal("expected fallback discoverer to run when primary is missing") + } + + if _, ok := service.Resolve("gateway.charon.lthn"); !ok { + t.Fatal("expected fallback record to resolve") + } +} + func TestServiceDiscoverReturnsNilWithoutDiscoverer(t *testing.T) { service := NewService(ServiceOptions{ Records: map[string]NameRecords{ -- 2.45.3