366 lines
12 KiB
Go
366 lines
12 KiB
Go
package server
|
|
|
|
import (
|
|
"net"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/miekg/dns"
|
|
)
|
|
|
|
// readTick mirrors the unexported read deadline serveUDP uses. The test has to
|
|
// know it to be able to sleep past it; a copy here means a change to the
|
|
// product constant would quietly weaken this test rather than break it, so the
|
|
// value is asserted against the listener's behaviour below.
|
|
const readTick = 500 * time.Millisecond
|
|
|
|
// TestUDPReadDeadlineIsResetEveryIteration is the regression test for the
|
|
// worst bug this package has had (AGENTS.md): serveUDP sets a read deadline so
|
|
// the loop notices cancellation, and a deadline set ONCE expires for good.
|
|
// After that the loop spins — every ReadFrom returns "i/o timeout" instantly —
|
|
// and never reads another packet for the life of the process. The listener
|
|
// looks healthy: it is bound, it is in the admin API, it logs nothing.
|
|
//
|
|
// The invariant pinned here: a packet that arrives more than one read-tick
|
|
// after the previous one is still answered. Send, sleep past the tick, send
|
|
// again, and both must come back.
|
|
func TestUDPReadDeadlineIsResetEveryIteration(t *testing.T) {
|
|
h := newHarness(t, nil)
|
|
pc, err := net.ListenPacket("udp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listen: %v", err)
|
|
}
|
|
defer pc.Close()
|
|
addr, err := net.ResolveUDPAddr("udp", h.addr("udp"))
|
|
if err != nil {
|
|
t.Fatalf("resolve: %v", err)
|
|
}
|
|
|
|
ask := func(id uint16) {
|
|
t.Helper()
|
|
q := question("deadline.test.", dns.TypeA)
|
|
q.Id = id
|
|
wire, err := q.Pack()
|
|
if err != nil {
|
|
t.Fatalf("pack: %v", err)
|
|
}
|
|
if _, err := pc.WriteTo(wire, addr); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
_ = pc.SetReadDeadline(time.Now().Add(3 * time.Second))
|
|
buf := make([]byte, 4096)
|
|
n, _, err := pc.ReadFrom(buf)
|
|
if err != nil {
|
|
t.Fatalf("no answer to query %d: %v", id, err)
|
|
}
|
|
res := new(dns.Msg)
|
|
if err := res.Unpack(buf[:n]); err != nil {
|
|
t.Fatalf("unpack: %v", err)
|
|
}
|
|
if res.Id != id {
|
|
t.Errorf("reply id = %d, want %d", res.Id, id)
|
|
}
|
|
if a := answerAddr(res); a != "203.0.113.9" {
|
|
t.Errorf("reply A = %q, want 203.0.113.9", a)
|
|
}
|
|
}
|
|
|
|
ask(0x3001)
|
|
// Longer than the read deadline, so a deadline that is not reset has
|
|
// certainly expired by the time the second packet lands.
|
|
time.Sleep(readTick + 250*time.Millisecond)
|
|
ask(0x3002)
|
|
|
|
waitFor(t, 2*time.Second, "both queries at the engine", func() bool {
|
|
return len(h.fake.seen()) == 2
|
|
})
|
|
}
|
|
|
|
// TestUDPTruncatesAnOversizedAnswer: a multi-kilobyte answer to a client that
|
|
// advertised nothing (the 512-byte floor) must come back as a question-only
|
|
// TC reply. Sending it whole would be silently dropped by the client and read
|
|
// as a dropped query, which is exactly the bug the engine suite found once
|
|
// already (STATUS.md: "the uncached path never truncated").
|
|
func TestUDPTruncatesAnOversizedAnswer(t *testing.T) {
|
|
h := newHarness(t, nil)
|
|
res := h.askUDP(question("big.test.", dns.TypeTXT))
|
|
if !res.Truncated {
|
|
t.Error("TC not set on a 3 KiB answer to a 512-byte client")
|
|
}
|
|
if len(res.Answer) != 0 {
|
|
t.Errorf("truncated reply carries %d answers, want none", len(res.Answer))
|
|
}
|
|
if len(res.Question) != 1 || res.Question[0].Name != "big.test." {
|
|
t.Errorf("truncated reply question = %+v, want big.test.", res.Question)
|
|
}
|
|
if got := h.srv.load("udp", "truncated"); got != 1 {
|
|
t.Errorf("udp.truncated = %d, want 1", got)
|
|
}
|
|
if err := h.fake.lastPackErr(); err != nil {
|
|
t.Fatalf("the fake's big answer did not pack, so this test is measuring a fixture failure: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestUDPUsesTheAdvertisedEDNSSize: with EDNS0 the client says how much it can
|
|
// take, and the server must honour it — the same answer, delivered whole.
|
|
func TestUDPUsesTheAdvertisedEDNSSize(t *testing.T) {
|
|
h := newHarness(t, nil)
|
|
m := question("big.test.", dns.TypeTXT)
|
|
m.SetEdns0(4096, false)
|
|
res := h.askUDP(m)
|
|
if res.Truncated {
|
|
t.Error("3 KiB answer truncated for a client that advertised 4096")
|
|
}
|
|
if len(res.Answer) != 1 {
|
|
t.Fatalf("reply has %d answers, want the whole TXT", len(res.Answer))
|
|
}
|
|
if o := res.IsEdns0(); o == nil {
|
|
t.Error("reply has no OPT even though the query had one")
|
|
}
|
|
}
|
|
|
|
// TestUDPCapsTheAdvertisedEDNS0Buffer is the amplification guard: a client may
|
|
// claim 64 KiB, and the server rewrites the request to 4096 before the engine
|
|
// ever sees it, so no answer is built for a buffer nobody will accept.
|
|
func TestUDPCapsTheAdvertisedEDNS0Buffer(t *testing.T) {
|
|
h := newHarness(t, nil)
|
|
m := question("edns.test.", dns.TypeA)
|
|
m.SetEdns0(65535, false)
|
|
h.askUDP(m)
|
|
|
|
got := h.fake.only(t)
|
|
if got.ednsSize == 0 {
|
|
t.Fatal("the engine saw no EDNS0 at all")
|
|
}
|
|
if got.ednsSize > 4096 {
|
|
t.Errorf("engine saw an advertised buffer of %d, want it capped at 4096", got.ednsSize)
|
|
}
|
|
if got.ednsSize != 4096 {
|
|
t.Errorf("advertised buffer = %d, want the 4096 cap", got.ednsSize)
|
|
}
|
|
}
|
|
|
|
// TestFitUDP covers the packing helper directly, where the buffer arithmetic is
|
|
// visible without a socket in the way. The three cases are the three answers
|
|
// the function can give: pass through, truncate to the question, or refuse to
|
|
// answer at all.
|
|
func TestFitUDP(t *testing.T) {
|
|
t.Run("small answer passes through untouched", func(t *testing.T) {
|
|
s := &Server{}
|
|
q := question("small.test.", dns.TypeA)
|
|
res := answerA(newReply(q))
|
|
out := s.fitUDP(res, q, 512)
|
|
want, err := res.Pack()
|
|
if err != nil {
|
|
t.Fatalf("pack: %v", err)
|
|
}
|
|
if string(out) != string(want) {
|
|
t.Errorf("fitUDP rewrote an answer that already fit (%d vs %d bytes)", len(out), len(want))
|
|
}
|
|
if got := s.load("udp", "truncated"); got != 0 {
|
|
t.Errorf("truncated = %d for an answer that fit", got)
|
|
}
|
|
})
|
|
|
|
t.Run("oversized answer becomes a question-only TC reply", func(t *testing.T) {
|
|
s := &Server{}
|
|
q := question("big.test.", dns.TypeTXT)
|
|
res := bigAnswer(newReply(q))
|
|
full, err := res.Pack()
|
|
if err != nil {
|
|
t.Fatalf("the big answer must pack for this test to mean anything: %v", err)
|
|
}
|
|
if len(full) <= 512 {
|
|
t.Fatalf("fixture is only %d bytes, it cannot test truncation", len(full))
|
|
}
|
|
out := s.fitUDP(res, q, 512)
|
|
if len(out) > 512 {
|
|
t.Errorf("truncated reply is %d bytes, over the 512 floor", len(out))
|
|
}
|
|
got := new(dns.Msg)
|
|
if err := got.Unpack(out); err != nil {
|
|
t.Fatalf("unpack truncated reply: %v", err)
|
|
}
|
|
if !got.Truncated {
|
|
t.Error("TC not set")
|
|
}
|
|
if len(got.Answer) != 0 {
|
|
t.Errorf("truncated reply carries %d answers", len(got.Answer))
|
|
}
|
|
if len(got.Question) != 1 || got.Question[0].Name != q.Question[0].Name {
|
|
t.Errorf("question = %+v, want %v", got.Question, q.Question)
|
|
}
|
|
if got.Rcode != res.Rcode {
|
|
t.Errorf("rcode = %d, want the answer's %d", got.Rcode, res.Rcode)
|
|
}
|
|
if s.load("udp", "truncated") != 1 {
|
|
t.Errorf("truncated counter = %d, want 1", s.load("udp", "truncated"))
|
|
}
|
|
})
|
|
|
|
t.Run("an advertised EDNS0 buffer is the limit", func(t *testing.T) {
|
|
s := &Server{}
|
|
q := question("big.test.", dns.TypeTXT)
|
|
q.SetEdns0(4096, false)
|
|
res := bigAnswer(newReply(q))
|
|
out := s.fitUDP(res, q, 512)
|
|
got := new(dns.Msg)
|
|
if err := got.Unpack(out); err != nil {
|
|
t.Fatalf("unpack: %v", err)
|
|
}
|
|
if got.Truncated {
|
|
t.Error("3 KiB answer truncated against a 4096-byte buffer")
|
|
}
|
|
if len(got.Answer) != 1 {
|
|
t.Errorf("answers = %d, want 1", len(got.Answer))
|
|
}
|
|
if o := got.IsEdns0(); o == nil || o.UDPSize() != 4096 {
|
|
t.Errorf("truncated reply lost the OPT size: %+v", o)
|
|
}
|
|
})
|
|
|
|
t.Run("an unadvertised buffer below 512 cannot raise the floor", func(t *testing.T) {
|
|
s := &Server{}
|
|
q := question("big.test.", dns.TypeTXT)
|
|
// 300 is below the 512 floor RFC 1035 guarantees; using it as the
|
|
// limit would produce a reply no client is obliged to read.
|
|
q.SetEdns0(300, false)
|
|
res := bigAnswer(newReply(q))
|
|
out := s.fitUDP(res, q, 512)
|
|
if len(out) > 512 {
|
|
t.Errorf("reply is %d bytes, over the 512 floor", len(out))
|
|
}
|
|
got := new(dns.Msg)
|
|
if err := got.Unpack(out); err != nil {
|
|
t.Fatalf("unpack: %v", err)
|
|
}
|
|
if !got.Truncated {
|
|
t.Error("TC not set")
|
|
}
|
|
})
|
|
|
|
t.Run("an answer that will not pack is dropped, not truncated", func(t *testing.T) {
|
|
s := &Server{}
|
|
q := question("unpackable.test.", dns.TypeTXT)
|
|
res := new(dns.Msg)
|
|
res.SetReply(q)
|
|
// A character-string over 255 bytes: miekg/dns v1.1.73 reports its
|
|
// length as zero, so Pack fails. Dropping is the only safe answer —
|
|
// a half-written datagram is worse than none.
|
|
res.Answer = []dns.RR{&dns.TXT{
|
|
Hdr: dns.RR_Header{Name: q.Question[0].Name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 60},
|
|
Txt: []string{strings.Repeat("x", 300)},
|
|
}}
|
|
if _, err := res.Pack(); err == nil {
|
|
t.Fatal("fixture packs; this case is no longer testing the pack failure")
|
|
}
|
|
if out := s.fitUDP(res, q, 512); out != nil {
|
|
t.Errorf("fitUDP returned %d bytes for an unpackable answer, want nil", len(out))
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestUDPDropSendsNothing: a dropped query is answered with silence, which on
|
|
// UDP means no datagram at all. The counter is the only evidence a client-side
|
|
// timeout was a policy decision.
|
|
func TestUDPDropSendsNothing(t *testing.T) {
|
|
h := newHarness(t, nil)
|
|
pc, err := net.ListenPacket("udp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listen: %v", err)
|
|
}
|
|
defer pc.Close()
|
|
addr, err := net.ResolveUDPAddr("udp", h.addr("udp"))
|
|
if err != nil {
|
|
t.Fatalf("resolve: %v", err)
|
|
}
|
|
wire, err := question("drop.test.", dns.TypeA).Pack()
|
|
if err != nil {
|
|
t.Fatalf("pack: %v", err)
|
|
}
|
|
if _, err := pc.WriteTo(wire, addr); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
_ = pc.SetReadDeadline(time.Now().Add(300 * time.Millisecond))
|
|
buf := make([]byte, 4096)
|
|
if n, _, err := pc.ReadFrom(buf); err == nil {
|
|
t.Errorf("a dropped query was answered with %d bytes", n)
|
|
}
|
|
waitFor(t, 2*time.Second, "the drop at the engine", func() bool { return len(h.fake.seen()) == 1 })
|
|
if got := h.srv.load("udp", "dropped"); got != 1 {
|
|
t.Errorf("udp.dropped = %d, want 1", got)
|
|
}
|
|
if got := h.srv.load("udp", "queries"); got != 1 {
|
|
t.Errorf("udp.queries = %d, want 1", got)
|
|
}
|
|
}
|
|
|
|
// TestUDPMalformedPacketIsCountedNotAnswered: an unparsable datagram has no ID
|
|
// and no question, so there is nothing legal to reply with. It must be counted
|
|
// and dropped — a server that echoes a FORMERR to arbitrary garbage is an
|
|
// amplifier.
|
|
func TestUDPMalformedPacketIsCountedNotAnswered(t *testing.T) {
|
|
h := newHarness(t, nil)
|
|
pc, err := net.ListenPacket("udp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listen: %v", err)
|
|
}
|
|
defer pc.Close()
|
|
addr, err := net.ResolveUDPAddr("udp", h.addr("udp"))
|
|
if err != nil {
|
|
t.Fatalf("resolve: %v", err)
|
|
}
|
|
if _, err := pc.WriteTo([]byte{0x01, 0x02, 0x03}, addr); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
waitFor(t, 2*time.Second, "the parse failure", func() bool { return h.srv.load("udp", "errors") == 1 })
|
|
if n := len(h.fake.seen()); n != 0 {
|
|
t.Errorf("garbage reached the engine %d times", n)
|
|
}
|
|
_ = pc.SetReadDeadline(time.Now().Add(300 * time.Millisecond))
|
|
buf := make([]byte, 4096)
|
|
if n, _, err := pc.ReadFrom(buf); err == nil {
|
|
t.Errorf("garbage was answered with %d bytes", n)
|
|
}
|
|
}
|
|
|
|
// TestUDPQuestionlessMessageReachesTheEngine: a parseable message with no
|
|
// question is answerable with FORMERR, and servePacket deliberately hands it
|
|
// to the engine rather than dropping it — the comment in the code says so and
|
|
// it is the difference between a client that learns why it was refused and a
|
|
// client that times out.
|
|
func TestUDPQuestionlessMessageReachesTheEngine(t *testing.T) {
|
|
h := newHarness(t, nil)
|
|
pc, err := net.ListenPacket("udp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listen: %v", err)
|
|
}
|
|
defer pc.Close()
|
|
addr, err := net.ResolveUDPAddr("udp", h.addr("udp"))
|
|
if err != nil {
|
|
t.Fatalf("resolve: %v", err)
|
|
}
|
|
// Header only: valid DNS, qdcount 0.
|
|
msg := []byte{0x12, 0x34, 0x01, 0x00, 0, 0, 0, 0, 0, 0, 0, 0}
|
|
if _, err := pc.WriteTo(msg, addr); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
_ = pc.SetReadDeadline(time.Now().Add(3 * time.Second))
|
|
buf := make([]byte, 4096)
|
|
n, _, err := pc.ReadFrom(buf)
|
|
if err != nil {
|
|
t.Fatalf("no answer to a questionless query: %v", err)
|
|
}
|
|
res := new(dns.Msg)
|
|
if err := res.Unpack(buf[:n]); err != nil {
|
|
t.Fatalf("unpack: %v", err)
|
|
}
|
|
if res.Rcode != dns.RcodeFormatError {
|
|
t.Errorf("rcode = %s, want FORMERR", dns.RcodeToString[res.Rcode])
|
|
}
|
|
if got := h.fake.only(t); got.name != "" {
|
|
t.Errorf("engine saw question %q, want none", got.name)
|
|
}
|
|
}
|