Files
ggjuku/internal/app/network_dns_test.go
2026-09-30 10:20:58 +08:00

118 lines
4.4 KiB
Go

package app
import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"net/http/httptest"
"reflect"
"sync/atomic"
"testing"
"time"
)
func writeDNSFixture(writer http.ResponseWriter, addresses ...string) {
answers := make([]map[string]any, 0, len(addresses))
for _, address := range addresses {
answers = append(answers, map[string]any{"type": 1, "TTL": 37, "data": address})
}
writer.Header().Set("Content-Type", "application/dns-json")
json.NewEncoder(writer).Encode(map[string]any{"Status": 0, "Answer": answers})
}
func TestDNSResolverRetriesUnsafeOrInvalidAnswers(t *testing.T) {
for _, test := range []struct {
name string
addresses []string
body string
status int
}{
{name: "fake-ip", addresses: []string{"198.18.0.42"}},
{name: "private", addresses: []string{"172.18.0.2"}},
{name: "loopback", addresses: []string{"127.0.0.1"}},
{name: "metadata", addresses: []string{"169.254.169.254"}},
{name: "carrier-network", addresses: []string{"100.64.0.1"}},
{name: "reserved", addresses: []string{"203.0.113.1"}},
{name: "mixed-private", addresses: []string{"93.184.216.35", "10.0.0.1"}},
{name: "mixed-fake-ip", addresses: []string{"93.184.216.35", "198.19.0.1"}},
{name: "invalid-address", addresses: []string{"93.184.216.35", "invalid address"}},
{name: "ipv6-in-a-record", addresses: []string{"2606:4700:4700::1111"}},
{name: "empty-answer"},
{name: "nxdomain", body: `{"Status":3}`},
{name: "malformed-json", body: `{"Status":`},
{name: "http-error", addresses: []string{"93.184.216.35"}, status: http.StatusServiceUnavailable},
} {
t.Run(test.name, func(t *testing.T) {
var firstCalls, secondCalls atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
query := request.URL.Query()
if query.Get("name") != "covers.example.org" || query.Get("type") != "A" || query.Get("edns_client_subnet") != dnsAlternateSubnet || request.Header.Get("Accept") != "application/dns-json" {
t.Error("unexpected DNS request", request.URL)
}
if request.URL.Path == "/first" {
firstCalls.Add(1)
if test.status != 0 {
writer.WriteHeader(test.status)
}
if test.body != "" {
io.WriteString(writer, test.body)
} else {
writeDNSFixture(writer, test.addresses...)
}
return
}
secondCalls.Add(1)
writeDNSFixture(writer, "93.184.216.34")
}))
defer server.Close()
standard := http.DefaultTransport.(*http.Transport).Clone()
standard.Proxy = nil
resolver := newDNSResolver(standard)
defer resolver.client.CloseIdleConnections()
resolver.endpoints = []string{server.URL + "/first", server.URL + "/second"}
for range 2 {
entry, err := resolver.lookupEntry(context.Background(), "covers.example.org", dnsAlternateSubnet)
if err != nil || !reflect.DeepEqual(entry.addresses, []string{"93.184.216.34"}) {
t.Fatal("DNS failed to continue to a valid public answer", entry.addresses, err)
}
if remaining := time.Until(entry.expires); remaining <= 0 || remaining > 37*time.Second {
t.Fatal("DNS cache ignored the answer TTL", remaining)
}
}
if firstCalls.Load() != 1 || secondCalls.Load() != 1 {
t.Fatal("DNS fallback did not reuse its successful cache", firstCalls.Load(), secondCalls.Load())
}
})
}
}
func TestDNSResolverDoesNotCacheUnsafeAnswers(t *testing.T) {
var calls atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
calls.Add(1)
writeDNSFixture(writer, "93.184.216.34", "198.18.0.42")
}))
defer server.Close()
standard := http.DefaultTransport.(*http.Transport).Clone()
standard.Proxy = nil
resolver := newDNSResolver(standard)
defer resolver.client.CloseIdleConnections()
resolver.endpoints = []string{server.URL + "/first", server.URL + "/second"}
for range 2 {
addresses, err := resolver.lookup(context.Background(), "covers.example.org")
if err == nil || len(addresses) != 0 {
t.Fatal("unsafe DNS answer was returned", addresses, err)
}
}
if calls.Load() != 4 {
t.Fatal("unsafe DNS answer was cached or prevented trying another service", calls.Load())
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
if _, err := resolver.lookup(ctx, "cancelled.example.org"); !errors.Is(err, context.Canceled) || calls.Load() != 4 {
t.Fatal("cancelled DNS lookup did not stop", calls.Load(), err)
}
}