Files
hetzner-api-dyndns-records-…/internal/netresolve/resolver_test.go
T

121 lines
4.1 KiB
Go

package netresolve
import (
"context"
"errors"
"net"
"net/netip"
"sync/atomic"
"testing"
"time"
)
type lookupFunc func(context.Context, string, string) ([]netip.Addr, error)
func (f lookupFunc) LookupNetIP(ctx context.Context, network, host string) ([]netip.Addr, error) {
return f(ctx, network, host)
}
func TestDNSOutageFallbackAndReturnToSystem(t *testing.T) {
t.Parallel()
var healthy atomic.Bool
var fallbacks atomic.Int32
r := &Resolver{timeout: time.Second}
r.system = lookupFunc(func(_ context.Context, network, host string) ([]netip.Addr, error) {
if network != "ip4" || host != "api.ipify.org" {
t.Errorf("unexpected query %s %s", network, host)
}
if healthy.Load() {
return []netip.Addr{netip.MustParseAddr("1.0.0.1")}, nil
}
return nil, &net.DNSError{Err: "server misbehaving", IsTemporary: true}
})
r.fallbacks = []lookup{lookupFunc(func(_ context.Context, network, host string) ([]netip.Addr, error) {
fallbacks.Add(1)
if network != "ip4" || host != "api.ipify.org." {
t.Errorf("fallback must use absolute family-specific query: %s %s", network, host)
}
return []netip.Addr{netip.MustParseAddr("1.1.1.1")}, nil
})}
for _, recovered := range []bool{false, true} {
healthy.Store(recovered)
ips, err := r.LookupNetIP(context.Background(), "ip4", "api.ipify.org")
want := "1.1.1.1"
if recovered {
want = "1.0.0.1"
}
if err != nil || len(ips) != 1 || ips[0].String() != want {
t.Fatalf("DNS recovery: %v %v", ips, err)
}
}
if fallbacks.Load() != 1 {
t.Fatal("system recovery did not stop fallback use")
}
}
func TestSlowDNSCannotBlockWorkingFallback(t *testing.T) {
t.Parallel()
slow := lookupFunc(func(ctx context.Context, _, _ string) ([]netip.Addr, error) { <-ctx.Done(); return nil, ctx.Err() })
r := &Resolver{system: slow, timeout: 20 * time.Millisecond, fallbacks: []lookup{slow, lookupFunc(func(context.Context, string, string) ([]netip.Addr, error) {
return []netip.Addr{netip.MustParseAddr("1.1.1.1")}, nil
})}}
started := time.Now()
ips, err := r.LookupNetIP(context.Background(), "ip4", "api.ipify.org")
if err != nil || len(ips) != 1 || time.Since(started) > time.Second {
t.Fatalf("blocked fallback: %v %v", ips, err)
}
r.fallbacks = []lookup{slow}
if _, err = r.LookupNetIP(context.Background(), "ip4", "api.ipify.org"); err == nil {
t.Fatal("total outage was hidden")
}
}
func TestDNSFallbackDoesNotOverrideNegativeAnswersOrLocalNames(t *testing.T) {
t.Parallel()
for _, test := range []struct {
host string
err error
}{
{"api.ipify.org", &net.DNSError{IsNotFound: true}},
{"api.ipify.org", errors.New("permanent DNS error")},
{"pi.hole", &net.DNSError{IsTemporary: true}},
{"private.example", &net.DNSError{IsTemporary: true}},
} {
r := &Resolver{timeout: time.Second, system: lookupFunc(func(context.Context, string, string) ([]netip.Addr, error) { return nil, test.err }), fallbacks: []lookup{lookupFunc(func(context.Context, string, string) ([]netip.Addr, error) {
t.Error("unexpected external query")
return nil, nil
})}}
if _, err := r.LookupNetIP(context.Background(), "ip4", test.host); !errors.Is(err, test.err) {
t.Fatalf("original error lost: %v", err)
}
}
}
func TestCanceledDNSDoesNotStartFallback(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
cancel()
r := &Resolver{timeout: time.Second, system: lookupFunc(func(ctx context.Context, _, _ string) ([]netip.Addr, error) { return nil, ctx.Err() }), fallbacks: []lookup{lookupFunc(func(context.Context, string, string) ([]netip.Addr, error) {
t.Error("fallback after cancellation")
return nil, nil
})}}
if _, err := r.LookupNetIP(ctx, "ip6", "api6.ipify.org"); !errors.Is(err, context.Canceled) {
t.Fatal(err)
}
}
func TestDialAddressesTriesNextAddress(t *testing.T) {
t.Parallel()
listener, err := net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
_, port, _ := net.SplitHostPort(listener.Addr().String())
conn, err := DialAddresses(context.Background(), "tcp4", []netip.Addr{netip.MustParseAddr("127.0.0.2"), netip.MustParseAddr("127.0.0.1")}, port)
if err != nil {
t.Fatal(err)
}
conn.Close()
}