feat(dns): compute cache tree root in health
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
b784bb7927
commit
703621fe8b
2 changed files with 88 additions and 1 deletions
45
service.go
45
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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue