From 703621fe8be80ac324c9be705a66fc913817ccaa Mon Sep 17 00:00:00 2001 From: Virgil Date: Fri, 3 Apr 2026 19:50:48 +0000 Subject: [PATCH] feat(dns): compute cache tree root in health Co-Authored-By: Virgil --- service.go | 45 ++++++++++++++++++++++++++++++++++++++++++++- service_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/service.go b/service.go index 197bdf5..ff65646 100644 --- a/service.go +++ b/service.go @@ -1,6 +1,8 @@ package dns import ( + "crypto/sha256" + "encoding/hex" "fmt" "net" "slices" @@ -30,6 +32,7 @@ type Service struct { mu sync.RWMutex records map[string]NameRecords reverseIndex map[string][]string + treeRoot string } type ServiceOptions struct { @@ -41,9 +44,11 @@ func NewService(options ServiceOptions) *Service { for name, record := range options.Records { cached[normalizeName(name)] = record } + treeRoot := computeTreeRoot(cached) return &Service{ records: cached, reverseIndex: buildReverseIndex(cached), + treeRoot: treeRoot, } } @@ -52,6 +57,7 @@ func (service *Service) SetRecord(name string, record NameRecords) { defer service.mu.Unlock() service.records[normalizeName(name)] = record service.reverseIndex = buildReverseIndex(service.records) + service.treeRoot = computeTreeRoot(service.records) } func (service *Service) RemoveRecord(name string) { @@ -59,6 +65,7 @@ func (service *Service) RemoveRecord(name string) { defer service.mu.Unlock() delete(service.records, normalizeName(name)) service.reverseIndex = buildReverseIndex(service.records) + service.treeRoot = computeTreeRoot(service.records) } func (service *Service) Resolve(name string) (ResolveAllResult, bool) { @@ -117,7 +124,7 @@ func (service *Service) Health() map[string]any { return map[string]any{ "status": "ready", "names_cached": len(service.records), - "tree_root": "stubbed", + "tree_root": service.treeRoot, } } @@ -192,6 +199,42 @@ func normalizeIP(ip string) string { return parsed.String() } +func computeTreeRoot(records map[string]NameRecords) string { + names := make([]string, 0, len(records)) + for name := range records { + names = append(names, name) + } + slices.Sort(names) + + var builder strings.Builder + for _, name := range names { + record := records[name] + builder.WriteString(name) + builder.WriteByte('\n') + builder.WriteString("A=") + builder.WriteString(serializeRecordValues(record.A)) + builder.WriteByte('\n') + builder.WriteString("AAAA=") + builder.WriteString(serializeRecordValues(record.AAAA)) + builder.WriteByte('\n') + builder.WriteString("TXT=") + builder.WriteString(serializeRecordValues(record.TXT)) + builder.WriteByte('\n') + builder.WriteString("NS=") + builder.WriteString(serializeRecordValues(record.NS)) + builder.WriteByte('\n') + } + + sum := sha256.Sum256([]byte(builder.String())) + return hex.EncodeToString(sum[:]) +} + +func serializeRecordValues(values []string) string { + copied := append([]string(nil), values...) + slices.Sort(copied) + return strings.Join(copied, ",") +} + func findWildcardMatch(name string, records map[string]NameRecords) (NameRecords, bool) { bestMatch := "" for candidate := range records { diff --git a/service_test.go b/service_test.go index b3c5a51..c3d06d8 100644 --- a/service_test.go +++ b/service_test.go @@ -140,3 +140,47 @@ func TestServiceResolveReverseUsesSetAndRemove(t *testing.T) { t.Fatal("expected removed reverse record to disappear") } } + +func TestServiceHealthUsesDeterministicTreeRootAndUpdatesOnMutations(t *testing.T) { + service := NewService(ServiceOptions{ + Records: map[string]NameRecords{ + "gateway.charon.lthn": { + A: []string{"10.10.10.10", "10.0.0.1"}, + NS: []string{"ns1.example.com"}, + }, + }, + }) + + health := service.Health() + root, ok := health["tree_root"].(string) + if !ok || root == "" || root == "stubbed" { + t.Fatalf("expected computed tree root, got %#v", health["tree_root"]) + } + + healthRepeating := NewService(ServiceOptions{ + Records: map[string]NameRecords{ + "gateway.charon.lthn": { + NS: []string{"ns1.example.com"}, + A: []string{"10.0.0.1", "10.10.10.10"}, + }, + }, + }).Health()["tree_root"].(string) + + if healthRepeating != root { + t.Fatalf("expected deterministic tree root, got %s and %s", healthRepeating, root) + } + + service.SetRecord("gateway.charon.lthn", NameRecords{ + A: []string{"10.10.10.11"}, + }) + updatedRoot, ok := service.Health()["tree_root"].(string) + if !ok || updatedRoot == root { + t.Fatalf("expected updated tree root after SetRecord, got %s", updatedRoot) + } + + service.RemoveRecord("gateway.charon.lthn") + removedRoot, ok := service.Health()["tree_root"].(string) + if !ok || removedRoot == updatedRoot { + t.Fatalf("expected updated tree root after RemoveRecord, got %s", removedRoot) + } +}