1 parent
cc7466823b
commit
e8c5b414d9
44 files changed
+7649
-344
No files matched your search
@@ -0,0 +1,581 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"io"
|
||||
"log/slog"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"git.k8s-git-k6n.net/mats/fancy-dns/internal/config"
|
||||
"git.k8s-git-k6n.net/mats/fancy-dns/internal/engine"
|
||||
)
|
||||
|
||||
// testVersion is what the fake server reports in the DoH Server header.
|
||||
const testVersion = "9.9.9-test"
|
||||
|
||||
// recorded is one request as the handler saw it. The question is copied out
|
||||
// of the *dns.Msg because the engine is allowed to mutate the message, and a
|
||||
// test that asserts on a pointer the pipeline may rewrite is a flake waiting
|
||||
// to happen.
|
||||
type recorded struct {
|
||||
transport string
|
||||
name string
|
||||
qtype uint16
|
||||
remote netip.Addr
|
||||
tcp bool
|
||||
names []string
|
||||
tags []string
|
||||
dohPath string
|
||||
ednsSize uint16
|
||||
}
|
||||
|
||||
// fakeHandler is the one Handler every transport funnels into. It records what
|
||||
// it was asked and answers from a fixed recipe, so a test can tell "the UDP
|
||||
// listener delivered this" from "the engine produced this".
|
||||
type fakeHandler struct {
|
||||
mu sync.Mutex
|
||||
reqs []recorded
|
||||
|
||||
inflight atomic.Int64
|
||||
flushes atomic.Uint64
|
||||
// packErr is the last failure to pack a response the handler built. A
|
||||
// response that will not pack is silently dropped on the write path and
|
||||
// presents as a client timeout, so it is recorded here rather than being
|
||||
// discovered three layers away.
|
||||
packErr error
|
||||
|
||||
// arrived is signalled on every Handle entry, so a test can wait for a
|
||||
// query to be *inside* the pipeline instead of sleeping and hoping.
|
||||
arrived chan struct{}
|
||||
|
||||
gate chan struct{} // non-nil while queries are parked
|
||||
release chan struct{} // closed at cleanup so a held gate cannot wedge shutdown
|
||||
|
||||
muGate sync.Mutex
|
||||
}
|
||||
|
||||
func newFakeHandler() *fakeHandler {
|
||||
return &fakeHandler{
|
||||
arrived: make(chan struct{}, 64),
|
||||
release: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Handle records the request and answers from the recipe below.
|
||||
func (f *fakeHandler) Handle(ctx context.Context, req engine.Request) engine.Response {
|
||||
f.inflight.Add(1)
|
||||
defer f.inflight.Add(-1)
|
||||
|
||||
rec := recorded{
|
||||
transport: req.Transport,
|
||||
remote: req.Remote,
|
||||
tcp: req.TCP,
|
||||
names: append([]string(nil), req.ClientNames...),
|
||||
tags: append([]string(nil), req.ClientTags...),
|
||||
dohPath: req.DoHPath,
|
||||
}
|
||||
if req.Msg != nil && len(req.Msg.Question) > 0 {
|
||||
rec.name = req.Msg.Question[0].Name
|
||||
rec.qtype = req.Msg.Question[0].Qtype
|
||||
if o := req.Msg.IsEdns0(); o != nil {
|
||||
rec.ednsSize = o.UDPSize()
|
||||
}
|
||||
}
|
||||
f.mu.Lock()
|
||||
f.reqs = append(f.reqs, rec)
|
||||
f.mu.Unlock()
|
||||
|
||||
select {
|
||||
case f.arrived <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
|
||||
if g := f.gateCh(); g != nil {
|
||||
select {
|
||||
case <-g:
|
||||
case <-f.release:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}
|
||||
|
||||
res := f.answer(req.Msg, rec)
|
||||
if res.Msg != nil {
|
||||
if _, err := res.Msg.Pack(); err != nil {
|
||||
f.mu.Lock()
|
||||
f.packErr = err
|
||||
f.mu.Unlock()
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// answer is the canned response. The question name selects the outcome, so a
|
||||
// test drives a blocked/cached/dropped/big answer by asking for it by name
|
||||
// instead of reaching into the server. The reply is built from a copy of the
|
||||
// query: SetReply carries the query's ID across, and a reply that invents its
|
||||
// own ID is discarded by every real client.
|
||||
func (f *fakeHandler) answer(q *dns.Msg, rec recorded) engine.Response {
|
||||
m := newReply(q)
|
||||
switch {
|
||||
case strings.HasPrefix(rec.name, "block."):
|
||||
return engine.Response{Msg: blockedReply(m), Action: "block", BlockSource: "test"}
|
||||
case strings.HasPrefix(rec.name, "cached."):
|
||||
return engine.Response{Msg: answerA(m), Action: "cached", CacheHit: true, CacheKind: "hit"}
|
||||
case strings.HasPrefix(rec.name, "fail."):
|
||||
m.Rcode = dns.RcodeServerFailure
|
||||
return engine.Response{Msg: m, Action: "error"}
|
||||
case strings.HasPrefix(rec.name, "drop."):
|
||||
return engine.Response{Drop: true, Action: "drop"}
|
||||
case strings.HasPrefix(rec.name, "big."):
|
||||
return engine.Response{Msg: bigAnswer(m), Action: "allow"}
|
||||
case len(rec.name) == 0:
|
||||
m.Rcode = dns.RcodeFormatError
|
||||
return engine.Response{Msg: m, Action: "error"}
|
||||
}
|
||||
return engine.Response{Msg: answerA(m), Action: "allow"}
|
||||
}
|
||||
|
||||
func (f *fakeHandler) Inflight() int64 { return f.inflight.Load() }
|
||||
func (f *fakeHandler) Flush() { f.flushes.Add(1) }
|
||||
|
||||
// flushes returns how many times the admin flush action reached the engine.
|
||||
func (f *fakeHandler) flushCount() uint64 { return f.flushes.Load() }
|
||||
|
||||
// hold parks every handler that arrives from now on until the returned func is
|
||||
// called. It is how a test gets a query that is reliably in flight.
|
||||
func (f *fakeHandler) hold() func() {
|
||||
f.muGate.Lock()
|
||||
defer f.muGate.Unlock()
|
||||
if f.gate != nil {
|
||||
return func() {}
|
||||
}
|
||||
g := make(chan struct{})
|
||||
f.gate = g
|
||||
return func() { close(g) }
|
||||
}
|
||||
|
||||
func (f *fakeHandler) gateCh() chan struct{} {
|
||||
f.muGate.Lock()
|
||||
defer f.muGate.Unlock()
|
||||
return f.gate
|
||||
}
|
||||
|
||||
// waitArrived blocks until a request has reached the handler.
|
||||
func (f *fakeHandler) waitArrived(t *testing.T) {
|
||||
t.Helper()
|
||||
select {
|
||||
case <-f.arrived:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("no query reached the handler")
|
||||
}
|
||||
}
|
||||
|
||||
// lastPackErr reports the last response that failed to pack.
|
||||
func (f *fakeHandler) lastPackErr() error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
return f.packErr
|
||||
}
|
||||
|
||||
// seen returns a copy of everything recorded so far.
|
||||
func (f *fakeHandler) seen() []recorded {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
return append([]recorded(nil), f.reqs...)
|
||||
}
|
||||
|
||||
// only asserts that exactly one request was recorded and returns it.
|
||||
func (f *fakeHandler) only(t *testing.T) recorded {
|
||||
t.Helper()
|
||||
got := f.seen()
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("handler saw %d requests, want exactly 1: %+v", len(got), got)
|
||||
}
|
||||
return got[0]
|
||||
}
|
||||
|
||||
// last returns the most recent recorded request.
|
||||
func (f *fakeHandler) last() recorded {
|
||||
got := f.seen()
|
||||
if len(got) == 0 {
|
||||
return recorded{}
|
||||
}
|
||||
return got[len(got)-1]
|
||||
}
|
||||
|
||||
// reset drops the recording, so one server can serve several independent
|
||||
// questions.
|
||||
func (f *fakeHandler) reset() {
|
||||
f.mu.Lock()
|
||||
f.reqs = nil
|
||||
f.mu.Unlock()
|
||||
}
|
||||
|
||||
// newReply builds the shell of a reply: the query's ID, opcode, question and
|
||||
// OPT. A real engine's finalize step does the EDNS0 handshake and SetReply
|
||||
// carries neither the OPT nor a made-up ID, so a fake that skipped this would
|
||||
// be the only thing in the path dropping them.
|
||||
func newReply(q *dns.Msg) *dns.Msg {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(q.Copy())
|
||||
m.Authoritative = false
|
||||
if o := q.IsEdns0(); o != nil {
|
||||
m.SetEdns0(o.UDPSize(), o.Do())
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// answerA adds the canned A record to a prepared reply.
|
||||
func answerA(m *dns.Msg) *dns.Msg {
|
||||
if len(m.Question) > 0 && m.Question[0].Qtype == dns.TypeA {
|
||||
m.Answer = append(m.Answer, &dns.A{
|
||||
Hdr: dns.RR_Header{Name: m.Question[0].Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 60},
|
||||
A: net.ParseIP("203.0.113.9"),
|
||||
})
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// blockedReply adds the 0.0.0.0 sinkhole answer.
|
||||
func blockedReply(m *dns.Msg) *dns.Msg {
|
||||
if len(m.Question) > 0 && m.Question[0].Qtype == dns.TypeA {
|
||||
m.Answer = append(m.Answer, &dns.A{
|
||||
Hdr: dns.RR_Header{Name: m.Question[0].Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 60},
|
||||
A: net.ParseIP("0.0.0.0"),
|
||||
})
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// bigAnswer builds a multi-kilobyte answer out of many short TXT strings.
|
||||
// One long string does NOT work: miekg/dns v1.1.73 reports a character-string
|
||||
// over 255 bytes as zero length, so Msg.Pack fails with "dns: string exceeded
|
||||
// 255 bytes" and, on a server write path, fails silently. 15 x 200 lands just
|
||||
// over 3 KiB: far outside a 512-byte client buffer, inside the 4096 EDNS0
|
||||
// buffer the server accepts.
|
||||
func bigAnswer(m *dns.Msg) *dns.Msg {
|
||||
txt := make([]string, 15)
|
||||
for i := range txt {
|
||||
txt[i] = strings.Repeat("x", 200)
|
||||
}
|
||||
name := "."
|
||||
if len(m.Question) > 0 {
|
||||
name = m.Question[0].Name
|
||||
}
|
||||
m.Answer = append(m.Answer, &dns.TXT{
|
||||
Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 60},
|
||||
Txt: txt,
|
||||
})
|
||||
return m
|
||||
}
|
||||
|
||||
// harness is a started Server with all four listeners on loopback:0, a fake
|
||||
// engine behind them and a self-signed CA the test clients pin.
|
||||
type harness struct {
|
||||
t *testing.T
|
||||
cfg *config.Config
|
||||
store *config.Store
|
||||
fake *fakeHandler
|
||||
srv *Server
|
||||
pool *x509.CertPool
|
||||
cert tls.Certificate
|
||||
}
|
||||
|
||||
func newHarness(t *testing.T, mutate func(*config.Config)) *harness {
|
||||
t.Helper()
|
||||
certFile, keyFile := writeTestCert(t)
|
||||
pool := testCertPool(t, certFile)
|
||||
|
||||
cfg := config.Default()
|
||||
cfg.DNS.UDP = []string{"127.0.0.1:0"}
|
||||
cfg.DNS.TCP = []string{"127.0.0.1:0"}
|
||||
cfg.DNS.DoT = config.DoT{Enabled: true, Listen: []string{"127.0.0.1:0"}, CertFile: certFile, KeyFile: keyFile, MinVersion: "1.2"}
|
||||
cfg.DNS.DoH = config.DoH{Enabled: true, Listen: []string{"127.0.0.1:0"}, CertFile: certFile, KeyFile: keyFile, BasePath: "/dns-query", MinVersion: "1.2", AllowPost: true}
|
||||
cfg.DNS.DoH.Transport = config.Transport{Mode: "auto"}
|
||||
// A short stream idle timeout keeps a test that leaves a connection open
|
||||
// from holding a goroutine for the four-second default.
|
||||
cfg.Server.Timeouts.TLS = config.Duration(2 * time.Second)
|
||||
if mutate != nil {
|
||||
mutate(cfg)
|
||||
}
|
||||
|
||||
store := config.NewStore(cfg, "")
|
||||
fake := newFakeHandler()
|
||||
srv := New(Options{
|
||||
Engine: fake,
|
||||
Config: store,
|
||||
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
|
||||
Version: testVersion,
|
||||
})
|
||||
if err := srv.Start(context.Background()); err != nil {
|
||||
t.Fatalf("start: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
close(fake.release)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
_ = srv.Stop(ctx)
|
||||
})
|
||||
return &harness{t: t, cfg: cfg, store: store, fake: fake, srv: srv, pool: pool}
|
||||
}
|
||||
|
||||
// addr returns the bound address of one transport, read back from the
|
||||
// listener snapshot rather than guessed.
|
||||
func (h *harness) addr(transport string) string {
|
||||
h.t.Helper()
|
||||
for _, l := range h.srv.Listeners() {
|
||||
if l.Transport == transport {
|
||||
return l.Addr
|
||||
}
|
||||
}
|
||||
h.t.Fatalf("no %s listener; have %+v", transport, h.srv.Listeners())
|
||||
return ""
|
||||
}
|
||||
|
||||
func (h *harness) transports() []string {
|
||||
var out []string
|
||||
for _, l := range h.srv.Listeners() {
|
||||
out = append(out, l.Transport)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// base returns the normalised DoH base path.
|
||||
func (h *harness) base() string { return normaliseBase(h.cfg.DNS.DoH.BasePath) }
|
||||
|
||||
func (h *harness) clientTLS() *tls.Config {
|
||||
return &tls.Config{RootCAs: h.pool, MinVersion: tls.VersionTLS12}
|
||||
}
|
||||
|
||||
// writeTestCert generates a self-signed certificate for 127.0.0.1 into
|
||||
// t.TempDir and returns the two paths. Generating it in-test keeps the
|
||||
// encrypted-listener tests self-contained: no testdata to expire.
|
||||
func writeTestCert(t *testing.T) (certFile, keyFile string) {
|
||||
t.Helper()
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("key: %v", err)
|
||||
}
|
||||
tmpl := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{CommonName: "fancy-dns test"},
|
||||
NotBefore: time.Now().Add(-time.Hour),
|
||||
NotAfter: time.Now().Add(24 * time.Hour),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
DNSNames: []string{"localhost"},
|
||||
IPAddresses: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")},
|
||||
}
|
||||
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
|
||||
if err != nil {
|
||||
t.Fatalf("certificate: %v", err)
|
||||
}
|
||||
kb, err := x509.MarshalECPrivateKey(key)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal key: %v", err)
|
||||
}
|
||||
dir := t.TempDir()
|
||||
certFile = filepath.Join(dir, "cert.pem")
|
||||
keyFile = filepath.Join(dir, "key.pem")
|
||||
if err := os.WriteFile(certFile, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), 0o600); err != nil {
|
||||
t.Fatalf("write cert: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(keyFile, pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: kb}), 0o600); err != nil {
|
||||
t.Fatalf("write key: %v", err)
|
||||
}
|
||||
return certFile, keyFile
|
||||
}
|
||||
|
||||
// testCertPool returns a pool trusting only the generated certificate, so a
|
||||
// client that skips verification is a deliberate choice rather than a habit.
|
||||
func testCertPool(t *testing.T, certFile string) *x509.CertPool {
|
||||
t.Helper()
|
||||
pemBytes, err := os.ReadFile(certFile)
|
||||
if err != nil {
|
||||
t.Fatalf("read cert: %v", err)
|
||||
}
|
||||
pool := x509.NewCertPool()
|
||||
if !pool.AppendCertsFromPEM(pemBytes) {
|
||||
t.Fatal("test certificate did not parse into a pool")
|
||||
}
|
||||
return pool
|
||||
}
|
||||
|
||||
// ask builds a query and exchanges it over the named transport. The client
|
||||
// timeouts are short on purpose: a hung listener must fail the test in
|
||||
// seconds, not at the package timeout.
|
||||
func (h *harness) ask(transport string, name string, qtype uint16) *dns.Msg {
|
||||
h.t.Helper()
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion(dns.Fqdn(name), qtype)
|
||||
m.RecursionDesired = true
|
||||
switch transport {
|
||||
case "udp":
|
||||
return h.askUDP(m)
|
||||
case "tcp":
|
||||
return h.askTCP(m)
|
||||
case "dot":
|
||||
return h.askDoT(m)
|
||||
case "doh":
|
||||
return h.askDoHGET(m)
|
||||
case "doh-post":
|
||||
return h.askDoHPOST(m)
|
||||
}
|
||||
h.t.Fatalf("unknown transport %q", transport)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *harness) askUDP(m *dns.Msg) *dns.Msg {
|
||||
h.t.Helper()
|
||||
c := &dns.Client{Net: "udp", Timeout: 3 * time.Second}
|
||||
res, _, err := c.Exchange(m, h.addr("udp"))
|
||||
if err != nil {
|
||||
h.t.Fatalf("udp exchange: %v", err)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (h *harness) askTCP(m *dns.Msg) *dns.Msg {
|
||||
h.t.Helper()
|
||||
c := &dns.Client{Net: "tcp", Timeout: 3 * time.Second}
|
||||
res, _, err := c.Exchange(m, h.addr("tcp"))
|
||||
if err != nil {
|
||||
h.t.Fatalf("tcp exchange: %v", err)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (h *harness) askDoT(m *dns.Msg) *dns.Msg {
|
||||
h.t.Helper()
|
||||
tc := h.clientTLS()
|
||||
tc.NextProtos = []string{"dot"}
|
||||
c := &dns.Client{Net: "tcp-tls", Timeout: 3 * time.Second, TLSConfig: tc}
|
||||
res, _, err := c.Exchange(m, h.addr("dot"))
|
||||
if err != nil {
|
||||
h.t.Fatalf("dot exchange: %v", err)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (h *harness) dohURL(path string) string {
|
||||
return "https://" + h.addr("doh") + path
|
||||
}
|
||||
|
||||
func (h *harness) dohClient(forceH2 bool) *http.Client {
|
||||
tr := &http.Transport{TLSClientConfig: h.clientTLS(), ForceAttemptHTTP2: forceH2}
|
||||
h.t.Cleanup(tr.CloseIdleConnections)
|
||||
return &http.Client{Transport: tr, Timeout: 5 * time.Second}
|
||||
}
|
||||
|
||||
// askDoHGET issues the RFC 8484 GET profile with the wire message in ?dns=.
|
||||
func (h *harness) askDoHGET(m *dns.Msg) *dns.Msg {
|
||||
h.t.Helper()
|
||||
res, body := h.doHGET(m)
|
||||
if res.StatusCode != http.StatusOK {
|
||||
h.t.Fatalf("doh GET status %d: %s", res.StatusCode, body)
|
||||
}
|
||||
return unpackDoH(h.t, body)
|
||||
}
|
||||
|
||||
// askDoHPOST issues the RFC 8484 POST profile.
|
||||
func (h *harness) askDoHPOST(m *dns.Msg) *dns.Msg {
|
||||
h.t.Helper()
|
||||
res, body := h.doHPOST(m)
|
||||
if res.StatusCode != http.StatusOK {
|
||||
h.t.Fatalf("doh POST status %d: %s", res.StatusCode, body)
|
||||
}
|
||||
return unpackDoH(h.t, body)
|
||||
}
|
||||
|
||||
func (h *harness) doHGET(m *dns.Msg) (*http.Response, []byte) {
|
||||
h.t.Helper()
|
||||
wire, err := m.Pack()
|
||||
if err != nil {
|
||||
h.t.Fatalf("pack query: %v", err)
|
||||
}
|
||||
u := h.dohURL(h.base() + "?dns=" + base64.RawURLEncoding.EncodeToString(wire))
|
||||
res, err := h.dohClient(false).Get(u)
|
||||
if err != nil {
|
||||
h.t.Fatalf("doh GET: %v", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
return res, body
|
||||
}
|
||||
|
||||
func (h *harness) doHPOST(m *dns.Msg) (*http.Response, []byte) {
|
||||
h.t.Helper()
|
||||
return h.doHPostWith(m, contentTypeDNS)
|
||||
}
|
||||
|
||||
func (h *harness) doHPostWith(m *dns.Msg, contentType string) (*http.Response, []byte) {
|
||||
h.t.Helper()
|
||||
wire, err := m.Pack()
|
||||
if err != nil {
|
||||
h.t.Fatalf("pack query: %v", err)
|
||||
}
|
||||
res, err := h.dohClient(false).Post(h.dohURL(h.base()), contentType, strings.NewReader(string(wire)))
|
||||
if err != nil {
|
||||
h.t.Fatalf("doh POST: %v", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
return res, body
|
||||
}
|
||||
|
||||
func unpackDoH(t *testing.T, body []byte) *dns.Msg {
|
||||
t.Helper()
|
||||
m := new(dns.Msg)
|
||||
if err := m.Unpack(body); err != nil {
|
||||
t.Fatalf("unpack doh body (%d bytes): %v", len(body), err)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// waitFor polls cond until it holds or the deadline passes. Every wait in this
|
||||
// suite is bounded: a hang looks exactly like a suite failure.
|
||||
func waitFor(t *testing.T, timeout time.Duration, what string, cond func() bool) {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(timeout)
|
||||
for {
|
||||
if cond() {
|
||||
return
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatalf("timed out after %s waiting for %s", timeout, what)
|
||||
}
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// question builds a query the fake's recipe understands.
|
||||
func question(name string, qtype uint16) *dns.Msg {
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion(dns.Fqdn(name), qtype)
|
||||
m.RecursionDesired = true
|
||||
return m
|
||||
}
|
||||
|
||||
var _ = engine.Request{}
|
||||
Reference in new issue
Block a user