From 1cd25d5007426c7a03d585fe32eb8dbb1a64e7a1 Mon Sep 17 00:00:00 2001 From: Virgil Date: Fri, 3 Apr 2026 19:57:21 +0000 Subject: [PATCH] feat(dns): add chain alias discovery with hsd fallback Co-Authored-By: Virgil --- service.go | 74 ++++++++++++++++++++++++++++++-------- service_test.go | 96 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+), 14 deletions(-) diff --git a/service.go b/service.go index c5075b7..6047f56 100644 --- a/service.go +++ b/service.go @@ -30,18 +30,22 @@ 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) - fallbackDiscoverer 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) + chainAliasDiscoverer func(context.Context) ([]string, error) + fallbackChainAliasDiscoverer func(context.Context) ([]string, error) } type ServiceOptions struct { - Records map[string]NameRecords - Discoverer func() (map[string]NameRecords, error) - FallbackDiscoverer func() (map[string]NameRecords, error) + Records map[string]NameRecords + Discoverer func() (map[string]NameRecords, error) + FallbackDiscoverer func() (map[string]NameRecords, error) + ChainAliasDiscoverer func(context.Context) ([]string, error) + FallbackChainAliasDiscoverer func(context.Context) ([]string, error) } func NewService(options ServiceOptions) *Service { @@ -51,14 +55,56 @@ func NewService(options ServiceOptions) *Service { } treeRoot := computeTreeRoot(cached) return &Service{ - records: cached, - reverseIndex: buildReverseIndex(cached), - treeRoot: treeRoot, - discoverer: options.Discoverer, - fallbackDiscoverer: options.FallbackDiscoverer, + records: cached, + reverseIndex: buildReverseIndex(cached), + treeRoot: treeRoot, + discoverer: options.Discoverer, + fallbackDiscoverer: options.FallbackDiscoverer, + chainAliasDiscoverer: options.ChainAliasDiscoverer, + fallbackChainAliasDiscoverer: options.FallbackChainAliasDiscoverer, } } +func (service *Service) DiscoverFromChainAliases(ctx context.Context, client *HSDClient) error { + if client == nil { + return fmt.Errorf("hsd client is required") + } + + aliases, err := discoverAliases(ctx, service.chainAliasDiscoverer, service.fallbackChainAliasDiscoverer) + if err != nil { + return err + } + if aliases == nil { + return nil + } + return service.DiscoverWithHSD(ctx, aliases, client) +} + +func discoverAliases(ctx context.Context, discoverer func(context.Context) ([]string, error), fallback func(context.Context) ([]string, error)) ([]string, error) { + if discoverer == nil { + if fallback == nil { + return nil, nil + } + aliases, err := fallback(ctx) + if err != nil { + return nil, err + } + return aliases, nil + } + + aliases, err := discoverer(ctx) + if err != nil { + if fallback == nil { + return nil, err + } + aliases, err = fallback(ctx) + if err != nil { + return nil, err + } + } + return aliases, nil +} + func (service *Service) Discover() error { discoverer := service.discoverer fallback := service.fallbackDiscoverer diff --git a/service_test.go b/service_test.go index 90a6b80..1029ffe 100644 --- a/service_test.go +++ b/service_test.go @@ -1,7 +1,11 @@ package dns import ( + "context" + "encoding/json" "errors" + "net/http" + "net/http/httptest" "strings" "testing" "time" @@ -332,6 +336,98 @@ func TestServiceDiscoverUsesFallbackOnlyWhenPrimaryMissing(t *testing.T) { } } +func TestServiceDiscoverFromChainAliasesUsesFallbackWhenPrimaryFails(t *testing.T) { + primaryCalled := false + fallbackCalled := false + + server := httptest.NewServer(http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) { + var payload struct { + Method string `json:"method"` + Params []any `json:"params"` + } + if err := json.NewDecoder(request.Body).Decode(&payload); err != nil { + t.Fatalf("unexpected request payload: %v", err) + } + switch payload.Params[0] { + case "gateway.charon.lthn": + _ = json.NewEncoder(responseWriter).Encode(map[string]any{ + "result": map[string]any{ + "a": []string{"10.10.10.10"}, + }, + }) + case "node.charon.lthn": + _ = json.NewEncoder(responseWriter).Encode(map[string]any{ + "result": map[string]any{ + "aaaa": []string{"2600:1f1c:7f0:4f01::2"}, + }, + }) + default: + t.Fatalf("unexpected alias query: %#v", payload.Params) + } + })) + defer server.Close() + + service := NewService(ServiceOptions{ + ChainAliasDiscoverer: func(_ context.Context) ([]string, error) { + primaryCalled = true + return nil, errors.New("blockchain service unavailable") + }, + FallbackChainAliasDiscoverer: func(_ context.Context) ([]string, error) { + fallbackCalled = true + return []string{"gateway.charon.lthn", "node.charon.lthn"}, nil + }, + }) + + client := NewHSDClient(HSDClientOptions{ + URL: server.URL, + }) + if err := service.DiscoverFromChainAliases(context.Background(), client); err != nil { + t.Fatalf("expected chain alias discovery to complete: %v", err) + } + if !primaryCalled { + t.Fatal("expected primary chain alias discoverer to be attempted") + } + if !fallbackCalled { + t.Fatal("expected fallback chain alias discoverer to run") + } + + gateway, ok := service.Resolve("gateway.charon.lthn") + if !ok || len(gateway.A) != 1 || gateway.A[0] != "10.10.10.10" { + t.Fatalf("expected gateway A record, got %#v (ok=%t)", gateway, ok) + } + node, ok := service.Resolve("node.charon.lthn") + if !ok || len(node.AAAA) != 1 || node.AAAA[0] != "2600:1f1c:7f0:4f01::2" { + t.Fatalf("expected node AAAA record, got %#v (ok=%t)", node, ok) + } +} + +func TestServiceDiscoverFromChainAliasesIgnoresMissingDiscoverers(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) { + t.Fatalf("expected no hsd requests when alias discoverers are missing") + })) + defer server.Close() + + service := NewService(ServiceOptions{ + Records: map[string]NameRecords{ + "gateway.charon.lthn": { + A: []string{"10.10.10.10"}, + }, + }, + }) + client := NewHSDClient(HSDClientOptions{ + URL: server.URL, + }) + + if err := service.DiscoverFromChainAliases(context.Background(), client); err != nil { + t.Fatalf("expected no-op when no alias discoverer configured: %v", err) + } + + result, ok := service.Resolve("gateway.charon.lthn") + if !ok || len(result.A) != 1 || result.A[0] != "10.10.10.10" { + t.Fatalf("expected baseline record to stay in cache, got %#v (ok=%t)", result, ok) + } +} + func TestServiceDiscoverReturnsNilWithoutDiscoverer(t *testing.T) { service := NewService(ServiceOptions{ Records: map[string]NameRecords{ -- 2.45.3