feat(dns): add wildcard record resolution

Implements wildcard DNS resolution with most-specific suffix matching for dns.resolve, dns.resolve.txt, and dns.resolve.all paths.

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-03 19:48:20 +00:00
parent dcf8601296
commit 4280edbfb0
3 changed files with 253 additions and 0 deletions

3
go.mod Normal file
View file

@ -0,0 +1,3 @@
module dappco.re/go/dns
go 1.22

186
service.go Normal file
View file

@ -0,0 +1,186 @@
package dns
import (
"fmt"
"slices"
"strings"
"sync"
)
type NameRecords struct {
A []string `json:"a"`
AAAA []string `json:"aaaa"`
TXT []string `json:"txt"`
NS []string `json:"ns"`
}
type ResolveAllResult struct {
A []string `json:"a"`
AAAA []string `json:"aaaa"`
TXT []string `json:"txt"`
NS []string `json:"ns"`
}
type Service struct {
mu sync.RWMutex
records map[string]NameRecords
}
type ServiceOptions struct {
Records map[string]NameRecords
}
func NewService(options ServiceOptions) *Service {
cached := make(map[string]NameRecords, len(options.Records))
for name, record := range options.Records {
cached[normalizeName(name)] = record
}
return &Service{
records: cached,
}
}
func (service *Service) SetRecord(name string, record NameRecords) {
service.mu.Lock()
defer service.mu.Unlock()
service.records[normalizeName(name)] = record
}
func (service *Service) RemoveRecord(name string) {
service.mu.Lock()
defer service.mu.Unlock()
delete(service.records, normalizeName(name))
}
func (service *Service) Resolve(name string) (ResolveAllResult, bool) {
record, ok := service.findRecord(name)
if !ok {
return ResolveAllResult{}, false
}
return resolveResult(record), true
}
func (service *Service) ResolveTXT(name string) ([]string, bool) {
record, ok := service.findRecord(name)
if !ok {
return nil, false
}
return append([]string(nil), record.TXT...), true
}
func (service *Service) ResolveAll(name string) (ResolveAllResult, bool) {
record, ok := service.findRecord(name)
if !ok {
return ResolveAllResult{}, false
}
return resolveResult(record), true
}
func (service *Service) Health() map[string]any {
service.mu.RLock()
defer service.mu.RUnlock()
return map[string]any{
"status": "ready",
"names_cached": len(service.records),
"tree_root": "stubbed",
}
}
func (service *Service) findRecord(name string) (NameRecords, bool) {
service.mu.RLock()
defer service.mu.RUnlock()
normalized := normalizeName(name)
if record, ok := service.records[normalized]; ok {
return record, true
}
match, ok := findWildcardMatch(normalized, service.records)
return match, ok
}
func resolveResult(record NameRecords) ResolveAllResult {
return ResolveAllResult{
A: append([]string(nil), record.A...),
AAAA: append([]string(nil), record.AAAA...),
TXT: append([]string(nil), record.TXT...),
NS: append([]string(nil), record.NS...),
}
}
func findWildcardMatch(name string, records map[string]NameRecords) (NameRecords, bool) {
bestMatch := ""
for candidate := range records {
if !strings.HasPrefix(candidate, "*.") {
continue
}
suffix := strings.TrimPrefix(candidate, "*.")
if wildcardMatches(suffix, name) {
if betterWildcardMatch(candidate, bestMatch) {
bestMatch = candidate
}
}
}
if bestMatch == "" {
return NameRecords{}, false
}
return records[bestMatch], true
}
func wildcardMatches(suffix, name string) bool {
parts := strings.Split(suffix, ".")
if len(parts) == 0 || len(name) <= len(suffix)+1 {
return false
}
if !strings.HasSuffix(name, "."+suffix) {
return false
}
return strings.Count(name[:len(name)-len(suffix)], ".") >= 1
}
func betterWildcardMatch(candidate, current string) bool {
if current == "" {
return true
}
remainingCandidate := strings.TrimPrefix(candidate, "*.")
remainingCurrent := strings.TrimPrefix(current, "*.")
if len(remainingCandidate) > len(remainingCurrent) {
return true
}
if len(remainingCandidate) == len(remainingCurrent) {
return candidate < current
}
return false
}
func normalizeName(name string) string {
trimmed := strings.TrimSpace(strings.ToLower(name))
if trimmed == "" {
return ""
}
if strings.HasSuffix(trimmed, ".") {
trimmed = strings.TrimSuffix(trimmed, ".")
}
return trimmed
}
func (service *Service) String() string {
return fmt.Sprintf("dns.Service{records=%d}", len(service.records))
}
func MergeRecords(values ...[]string) []string {
unique := []string{}
seen := map[string]bool{}
for _, batch := range values {
for _, value := range batch {
if seen[value] {
continue
}
seen[value] = true
unique = append(unique, value)
}
}
slices.Sort(unique)
return unique
}

64
service_test.go Normal file
View file

@ -0,0 +1,64 @@
package dns
import "testing"
func TestServiceResolveUsesExactNameBeforeWildcard(t *testing.T) {
service := NewService(ServiceOptions{
Records: map[string]NameRecords{
"*.charon.lthn": {
A: []string{"10.69.69.165"},
},
"gateway.charon.lthn": {
A: []string{"10.10.10.10"},
},
},
})
result, ok := service.Resolve("gateway.charon.lthn")
if !ok {
t.Fatal("expected exact record to resolve")
}
if len(result.A) != 1 || result.A[0] != "10.10.10.10" {
t.Fatalf("unexpected resolve result: %#v", result.A)
}
}
func TestServiceResolveUsesMostSpecificWildcard(t *testing.T) {
service := NewService(ServiceOptions{
Records: map[string]NameRecords{
"*.lthn": {
A: []string{"10.0.0.1"},
},
"*.charon.lthn": {
A: []string{"10.0.0.2"},
},
},
})
result, ok := service.Resolve("gateway.charon.lthn")
if !ok {
t.Fatal("expected wildcard record to resolve")
}
if len(result.A) != 1 || result.A[0] != "10.0.0.2" {
t.Fatalf("unexpected wildcard match: %#v", result.A)
}
}
func TestServiceResolveTXTUsesWildcard(t *testing.T) {
service := NewService(ServiceOptions{
Records: map[string]NameRecords{
"*.gateway.charon.lthn": {
TXT: []string{"v=lthn1 type=gateway"},
},
},
})
result, ok := service.ResolveTXT("node1.gateway.charon.lthn.")
if !ok {
t.Fatal("expected wildcard TXT record")
}
if len(result) != 1 || result[0] != "v=lthn1 type=gateway" {
t.Fatalf("unexpected TXT record: %#v", result)
}
}