detached 33a243c

dnsfwd_test.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package dnsfwd
4
5import (
6	"context"
7	"encoding/binary"
8	"errors"
9	"io"
10	"net"
11	"net/netip"
12	"os"
13	"path/filepath"
14	"reflect"
15	"strings"
16	"sync/atomic"
17	"syscall"
18	"testing"
19	"testing/synctest"
20	"time"
21)
22
23// query builds a minimal query for name with the given ID.
24func query(id uint16, name string) []byte {
25	b := make([]byte, hdrLen)
26	binary.BigEndian.PutUint16(b, id)
27	b[2] = 0x01 // RD
28	b[5] = 1    // QDCOUNT
29	for _, l := range strings.Split(name, ".") {
30		b = append(b, byte(len(l)))
31		b = append(b, l...)
32	}
33	return append(b, 0, 0, 1, 0, 1) // root, A, IN
34}
35
36// answer turns a query into a response carrying a recognisable payload.
37func answer(q []byte) []byte {
38	r := append([]byte(nil), q...)
39	r[2] |= 0x80
40	return append(r, "ANSWER"...)
41}
42
43// listenUDPAndTCP binds UDP on a free loopback port and TCP on the same port.
44// The TCP port may already be taken (outbound connections use the same
45// ephemeral range), so it picks again a few times, like Listen does.
46func listenUDPAndTCP(t *testing.T) (net.PacketConn, net.Listener) {
47	t.Helper()
48	for attempt := 0; ; attempt++ {
49		pc, err := net.ListenPacket("udp", "127.0.0.1:0")
50		if err != nil {
51			t.Skip("no loopback:", err)
52		}
53		tl, err := net.Listen("tcp", pc.LocalAddr().String())
54		if err == nil {
55			return pc, tl
56		}
57		_ = pc.Close()
58		if attempt == 49 || !errors.Is(err, syscall.EADDRINUSE) {
59			t.Fatal(err)
60		}
61	}
62}
63
64// fakeUpstream serves UDP and TCP on one port of 127.0.0.1 and returns it.
65func fakeUpstream(t *testing.T, handle func(q []byte) []byte) string {
66	t.Helper()
67	pc, tl := listenUDPAndTCP(t)
68	addr := pc.LocalAddr().String()
69	t.Cleanup(func() { _ = pc.Close(); _ = tl.Close() })
70	go func() {
71		buf := make([]byte, 4096)
72		for {
73			n, from, err := pc.ReadFrom(buf)
74			if err != nil {
75				return
76			}
77			if r := handle(append([]byte(nil), buf[:n]...)); r != nil {
78				_, _ = pc.WriteTo(r, from)
79			}
80		}
81	}()
82	go func() {
83		for {
84			c, err := tl.Accept()
85			if err != nil {
86				return
87			}
88			go func() {
89				defer func() { _ = c.Close() }()
90				var l [2]byte
91				if _, err := io.ReadFull(c, l[:]); err != nil {
92					return
93				}
94				q := make([]byte, binary.BigEndian.Uint16(l[:]))
95				if _, err := io.ReadFull(c, q); err != nil {
96					return
97				}
98				r := handle(q)
99				out := make([]byte, 2+len(r))
100				binary.BigEndian.PutUint16(out, uint16(len(r)))
101				copy(out[2:], r)
102				_, _ = c.Write(out)
103			}()
104		}
105	}()
106	return addr
107}
108
109func start(t *testing.T, ups ...string) (*Forwarder, string) {
110	t.Helper()
111	return startWith(t, nil, ups...)
112}
113
114// startWith is start with a hook to adjust the Forwarder before Listen.
115func startWith(t *testing.T, cfg func(*Forwarder), ups ...string) (*Forwarder, string) {
116	t.Helper()
117	f := New(func() []string { return ups }, 0)
118	if cfg != nil {
119		cfg(f)
120	}
121	ip := netip.MustParseAddr("127.0.0.1")
122	if err := f.Listen(context.Background(), ip); err != nil {
123		t.Fatal(err)
124	}
125	t.Cleanup(f.CloseAll)
126	a, _ := f.Addr(ip)
127	return f, a.String()
128}
129
130func TestUDPAndTCPForwarding(t *testing.T) {
131	up := fakeUpstream(t, answer)
132	_, addr := start(t, up)
133
134	c, err := net.Dial("udp", addr)
135	if err != nil {
136		t.Fatal(err)
137	}
138	defer func() { _ = c.Close() }()
139	_ = c.SetDeadline(time.Now().Add(5 * time.Second))
140	q := query(0x1234, "example.test")
141	_, _ = c.Write(q)
142	buf := make([]byte, 512)
143	n, err := c.Read(buf)
144	if err != nil {
145		t.Fatal(err)
146	}
147	if string(buf[n-6:n]) != "ANSWER" || binary.BigEndian.Uint16(buf) != 0x1234 {
148		t.Fatalf("udp answer: %x", buf[:n])
149	}
150
151	tc, err := net.Dial("tcp", addr)
152	if err != nil {
153		t.Fatal(err)
154	}
155	defer func() { _ = tc.Close() }()
156	_ = tc.SetDeadline(time.Now().Add(5 * time.Second))
157	// Two queries on one connection.
158	for _, id := range []uint16{7, 8} {
159		q := query(id, "tcp.test")
160		framed := append([]byte{byte(len(q) >> 8), byte(len(q))}, q...)
161		_, _ = tc.Write(framed)
162		var l [2]byte
163		if _, err := io.ReadFull(tc, l[:]); err != nil {
164			t.Fatal(err)
165		}
166		r := make([]byte, binary.BigEndian.Uint16(l[:]))
167		if _, err := io.ReadFull(tc, r); err != nil {
168			t.Fatal(err)
169		}
170		if binary.BigEndian.Uint16(r) != id || string(r[len(r)-6:]) != "ANSWER" {
171			t.Fatalf("tcp answer for %d: %x", id, r)
172		}
173	}
174}
175
176func TestFailoverToSecondUpstream(t *testing.T) {
177	dead := "127.0.0.1:1" // nothing listens: UDP read fails with ICMP refused
178	live := fakeUpstream(t, answer)
179	_, addr := start(t, dead, live)
180	c, _ := net.Dial("udp", addr)
181	defer func() { _ = c.Close() }()
182	_ = c.SetDeadline(time.Now().Add(8 * time.Second))
183	_, _ = c.Write(query(1, "a.test"))
184	buf := make([]byte, 512)
185	n, err := c.Read(buf)
186	if err != nil || string(buf[n-6:n]) != "ANSWER" {
187		t.Fatalf("n=%d err=%v", n, err)
188	}
189}
190
191func TestServfailWhenNoUpstreamAnswers(t *testing.T) {
192	_, addr := start(t) // no upstream at all
193	c, _ := net.Dial("udp", addr)
194	defer func() { _ = c.Close() }()
195	_ = c.SetDeadline(time.Now().Add(5 * time.Second))
196	_, _ = c.Write(query(0xbeef, "a.test"))
197	buf := make([]byte, 512)
198	n, err := c.Read(buf)
199	if err != nil {
200		t.Fatal(err)
201	}
202	if n != hdrLen || binary.BigEndian.Uint16(buf) != 0xbeef || buf[3]&0x0f != 2 || buf[2]&0x80 == 0 {
203		t.Fatalf("not a SERVFAIL: %x", buf[:n])
204	}
205}
206
207func TestShortPacketsIgnored(t *testing.T) {
208	up := fakeUpstream(t, answer)
209	_, addr := start(t, up)
210	c, _ := net.Dial("udp", addr)
211	defer func() { _ = c.Close() }()
212	_, _ = c.Write([]byte{1, 2, 3}) // dropped
213	_, _ = c.Write(query(9, "a.test"))
214	_ = c.SetDeadline(time.Now().Add(5 * time.Second))
215	buf := make([]byte, 512)
216	n, err := c.Read(buf)
217	if err != nil || binary.BigEndian.Uint16(buf) != 9 {
218		t.Fatalf("n=%d err=%v", n, err)
219	}
220}
221
222func TestListenTwiceAndClose(t *testing.T) {
223	f := New(func() []string { return nil }, 0)
224	ip := netip.MustParseAddr("127.0.0.1")
225	if err := f.Listen(context.Background(), ip); err != nil {
226		t.Skip(err)
227	}
228	if err := f.Listen(context.Background(), ip); err == nil {
229		t.Fatal("second Listen on the same address must fail")
230	}
231	a, _ := f.Addr(ip)
232	f.Close(ip)
233	f.Close(ip) // idempotent
234	if _, ok := f.Addr(ip); ok {
235		t.Fatal("still listening after Close")
236	}
237	if pc, err := net.ListenPacket("udp", a.String()); err != nil {
238		t.Fatalf("port not released: %v", err)
239	} else {
240		_ = pc.Close()
241	}
242}
243
244func TestServfailShape(t *testing.T) {
245	if servfail([]byte{1}) != nil {
246		t.Fatal("short query must give nil")
247	}
248	q := query(0xabcd, "x.test")
249	q[2] = 0x79 // opcode bits + RD + AA
250	r := servfail(q)
251	if r[0] != 0xab || r[1] != 0xcd || r[2]&0x80 == 0 || r[3] != 0x82 {
252		t.Fatalf("bad servfail %x", r)
253	}
254}
255
256func TestParseResolvConf(t *testing.T) {
257	in := `# comment
258nameserver 127.0.0.53
259nameserver 127.0.0.53
260nameserver 192.0.2.1 # trailing
261nameserver 2001:db8::1
262nameserver fe80::1%eth0
263nameserver 0.0.0.0
264nameserver not-an-ip
265search example.test
266options edns0
267nameserver
268`
269	got := ParseResolvConf(strings.NewReader(in))
270	want := []string{"127.0.0.53:53", "192.0.2.1:53", "[2001:db8::1]:53", "[fe80::1%eth0]:53"}
271	if !reflect.DeepEqual(got, want) {
272		t.Fatalf("got %v want %v", got, want)
273	}
274}
275
276func TestResolversReload(t *testing.T) {
277	p := filepath.Join(t.TempDir(), "resolv.conf")
278	if err := os.WriteFile(p, []byte("nameserver 192.0.2.1\n"), 0o644); err != nil {
279		t.Fatal(err)
280	}
281	r := NewResolvers(p)
282	if got := r.Addrs(); !reflect.DeepEqual(got, []string{"192.0.2.1:53"}) {
283		t.Fatal(got)
284	}
285	if err := os.WriteFile(p, []byte("nameserver 192.0.2.99\nnameserver 192.0.2.1\n"), 0o644); err != nil {
286		t.Fatal(err)
287	}
288	later := time.Now().Add(2 * time.Second)
289	_ = os.Chtimes(p, later, later)
290	if got := r.Addrs(); !reflect.DeepEqual(got, []string{"192.0.2.99:53", "192.0.2.1:53"}) {
291		t.Fatal(got)
292	}
293	// Unreadable file keeps the last known list.
294	_ = os.Remove(p)
295	if got := r.Addrs(); len(got) != 2 {
296		t.Fatalf("lost the last known list: %v", got)
297	}
298	if got := NewResolvers(filepath.Join(t.TempDir(), "missing")).Addrs(); len(got) != 0 {
299		t.Fatal(got)
300	}
301}
302
303// S2: a listener holds at most MaxTCPConns connections; extra ones are closed
304// at once, and a free slot is usable again after a close.
305func TestTCPConnectionCapPerListener(t *testing.T) {
306	up := fakeUpstream(t, answer)
307	_, addr := start(t, up)
308	var held []net.Conn
309	for range MaxTCPConns {
310		c, err := net.Dial("tcp", addr)
311		if err != nil {
312			t.Fatal(err)
313		}
314		held = append(held, c)
315		defer func() { _ = c.Close() }()
316	}
317	// Let the accept loop register all of them.
318	time.Sleep(200 * time.Millisecond)
319	extra, err := net.Dial("tcp", addr)
320	if err != nil {
321		t.Fatal(err)
322	}
323	defer func() { _ = extra.Close() }()
324	_ = extra.SetReadDeadline(time.Now().Add(3 * time.Second))
325	if _, err := extra.Read(make([]byte, 1)); err != io.EOF && !errors.Is(err, syscall.ECONNRESET) {
326		t.Fatalf("the connection over the cap must be closed by the server, got %v", err)
327	}
328	// Free one slot: a new connection is served.
329	_ = held[0].Close()
330	time.Sleep(200 * time.Millisecond)
331	c, err := net.Dial("tcp", addr)
332	if err != nil {
333		t.Fatal(err)
334	}
335	defer func() { _ = c.Close() }()
336	_ = c.SetDeadline(time.Now().Add(5 * time.Second))
337	q := query(3, "slot.test")
338	_, _ = c.Write(append([]byte{byte(len(q) >> 8), byte(len(q))}, q...))
339	var l [2]byte
340	if _, err := io.ReadFull(c, l[:]); err != nil {
341		t.Fatalf("a freed slot must serve again: %v", err)
342	}
343}
344
345// S2: the lifetime is absolute from accept: a client that keeps the connection
346// alive by trickling bytes still loses it.
347func TestTCPAbsoluteDeadline(t *testing.T) {
348	up := fakeUpstream(t, answer)
349	f := New(func() []string { return []string{up} }, 0)
350	f.Lifetime = 600 * time.Millisecond
351	ip := netip.MustParseAddr("127.0.0.1")
352	if err := f.Listen(context.Background(), ip); err != nil {
353		t.Skip(err)
354	}
355	t.Cleanup(f.CloseAll)
356	la, _ := f.Addr(ip)
357	addr := la.String()
358	c, err := net.Dial("tcp", addr)
359	if err != nil {
360		t.Fatal(err)
361	}
362	defer func() { _ = c.Close() }()
363	start := time.Now()
364	closed := make(chan struct{})
365	go func() {
366		_, _ = io.Copy(io.Discard, c) // returns when the server closes
367		close(closed)
368	}()
369	// Trickle one byte every 100 ms, never finishing a frame: only an idle
370	// timeout (10 s) would let this live; the absolute deadline must not.
371	go func() {
372		for i := 0; i < 40; i++ {
373			if _, err := c.Write([]byte{0}); err != nil {
374				return
375			}
376			time.Sleep(100 * time.Millisecond)
377		}
378	}()
379	select {
380	case <-closed:
381		if d := time.Since(start); d > 3*time.Second {
382			t.Fatalf("closed after %v, want about %v", d, f.Lifetime)
383		}
384	case <-time.After(5 * time.Second):
385		t.Fatal("a trickling client kept its connection past the absolute deadline")
386	}
387}
388
389// S2/S9: a saturated listener drops UDP queries instead of blocking its read
390// loop, and another listener (another guest) keeps answering.
391func TestUDPSaturationDropsAndIsolatesListeners(t *testing.T) {
392	release := make(chan struct{})
393	slow := fakeUpstream(t, func(q []byte) []byte { <-release; return answer(q) })
394	fast := fakeUpstream(t, answer)
395	var useSlow atomic.Bool
396	useSlow.Store(true)
397	f := New(func() []string {
398		if useSlow.Load() {
399			return []string{slow}
400		}
401		return []string{fast}
402	}, 0)
403	a1, a2 := netip.MustParseAddr("127.0.0.1"), netip.MustParseAddr("127.0.0.2")
404	if err := f.Listen(context.Background(), a1); err != nil {
405		t.Skip(err)
406	}
407	if err := f.Listen(context.Background(), a2); err != nil {
408		t.Skip(err)
409	}
410	t.Cleanup(f.CloseAll)
411	t.Cleanup(func() { close(release) })
412	l1, _ := f.Addr(a1)
413	flood, err := net.Dial("udp", l1.String())
414	if err != nil {
415		t.Fatal(err)
416	}
417	defer func() { _ = flood.Close() }()
418	for i := 0; i < MaxInflight*3; i++ {
419		_, _ = flood.Write(query(uint16(i), "flood.test"))
420	}
421	deadline := time.Now().Add(3 * time.Second)
422	for f.Dropped(a1) == 0 && time.Now().Before(deadline) {
423		time.Sleep(20 * time.Millisecond)
424	}
425	if f.Dropped(a1) == 0 {
426		t.Fatal("a saturated listener must drop queries")
427	}
428	// The other listener is unaffected: point the resolver at the fast upstream.
429	useSlow.Store(false)
430	l2, _ := f.Addr(a2)
431	c, err := net.Dial("udp", l2.String())
432	if err != nil {
433		t.Fatal(err)
434	}
435	defer func() { _ = c.Close() }()
436	_ = c.SetDeadline(time.Now().Add(4 * time.Second))
437	_, _ = c.Write(query(77, "other-guest.test"))
438	buf := make([]byte, 512)
439	n, err := c.Read(buf)
440	if err != nil || string(buf[n-6:n]) != "ANSWER" {
441		t.Fatalf("another listener must not be starved by the flood: n=%d err=%v", n, err)
442	}
443}
444
445// By default Listen fails fast on an address the host does not own (Docker and
446// the VM driver have configured it by then: a missing one is an error).
447func TestListenOnAMissingAddressFailsByDefault(t *testing.T) {
448	f := New(func() []string { return nil }, 0)
449	t.Cleanup(f.CloseAll)
450	ip := netip.MustParseAddr("203.0.113.77") // TEST-NET-3: never configured here
451	if err := f.Listen(context.Background(), ip); err == nil {
452		t.Fatal("listening on an address the host does not own must fail without FreeBind")
453	}
454}
455
456// With FreeBind (podman's netavark creates the bridge address only when the
457// first container starts) Listen binds an address the host does not own yet.
458func TestFreeBindListensOnAnAddressTheHostDoesNotOwnYet(t *testing.T) {
459	f := New(func() []string { return nil }, 0)
460	f.FreeBind = true
461	t.Cleanup(f.CloseAll)
462	ip := netip.MustParseAddr("203.0.113.77")
463	if err := f.Listen(context.Background(), ip); err != nil {
464		t.Fatalf("FreeBind: %v", err)
465	}
466	if _, ok := f.Addr(ip); !ok {
467		t.Fatal("no listener")
468	}
469}
470
471// queryType is query with a chosen type (and optionally EDNS0).
472func queryType(id uint16, name string, qtype uint16, edns bool) []byte {
473	q := query(id, name)
474	binary.BigEndian.PutUint16(q[len(q)-4:], qtype)
475	if edns {
476		q[11] = 1
477		q = append(q, 0, 0, 41, 0x10, 0, 0, 0, 0, 0, 0, 0)
478	}
479	return q
480}
481
482func udpExchange(t *testing.T, addr string, q []byte) []byte {
483	t.Helper()
484	c, err := net.Dial("udp", addr)
485	if err != nil {
486		t.Fatal(err)
487	}
488	defer func() { _ = c.Close() }()
489	_ = c.SetDeadline(time.Now().Add(5 * time.Second))
490	_, _ = c.Write(q)
491	buf := make([]byte, 1024)
492	n, err := c.Read(buf)
493	if err != nil {
494		t.Fatal(err)
495	}
496	return buf[:n]
497}
498
499func startOverride(t *testing.T, name string, ups ...string) (*Forwarder, string) {
500	t.Helper()
501	f := New(func() []string { return ups }, 0)
502	f.Override = name
503	ip := netip.MustParseAddr("127.0.0.1")
504	if err := f.Listen(context.Background(), ip); err != nil {
505		t.Fatal(err)
506	}
507	t.Cleanup(f.CloseAll)
508	a, _ := f.Addr(ip)
509	return f, a.String()
510}
511
512func TestOverrideAnswersAWithTheListenerAddress(t *testing.T) {
513	var asked atomic.Int64
514	up := fakeUpstream(t, func(q []byte) []byte { asked.Add(1); return answer(q) })
515	// A trailing dot in the setting is ignored; on the wire a name has none.
516	_, addr := startOverride(t, "Ermite.Example.org.", up)
517	for _, name := range []string{"ermite.example.org", "ERMITE.example.ORG", "Ermite.Example.Org"} {
518		r := udpExchange(t, addr, queryType(0x4242, name, 1, false))
519		if binary.BigEndian.Uint16(r) != 0x4242 || r[2]&0x80 == 0 || r[2]&0x01 == 0 || r[3]&0x0F != 0 {
520			t.Fatalf("%s: header %x", name, r[:hdrLen])
521		}
522		if an := binary.BigEndian.Uint16(r[6:]); an != 1 {
523			t.Fatalf("%s: %d answers", name, an)
524		}
525		if !strings.HasSuffix(string(r), "\x00\x04\x7f\x00\x00\x01") {
526			t.Fatalf("%s: rdata is not 127.0.0.1: %x", name, r)
527		}
528	}
529	if asked.Load() != 0 {
530		t.Fatalf("the override went upstream %d times", asked.Load())
531	}
532}
533
534func TestOverrideAAAAIsEmptyNoError(t *testing.T) {
535	up := fakeUpstream(t, answer)
536	_, addr := startOverride(t, "ermite.example.org", up)
537	for _, edns := range []bool{false, true} {
538		r := udpExchange(t, addr, queryType(7, "ermite.EXAMPLE.org", 28, edns))
539		if r[3]&0x0F != 0 || binary.BigEndian.Uint16(r[6:]) != 0 || r[2]&0x80 == 0 {
540			t.Fatalf("AAAA (edns %v): %x", edns, r)
541		}
542		if edns != (binary.BigEndian.Uint16(r[10:]) == 1) {
543			t.Fatalf("AAAA (edns %v): arcount %d", edns, binary.BigEndian.Uint16(r[10:]))
544		}
545	}
546}
547
548func TestOverrideForwardsEveryOtherName(t *testing.T) {
549	var asked atomic.Int64
550	up := fakeUpstream(t, func(q []byte) []byte { asked.Add(1); return answer(q) })
551	_, addr := startOverride(t, "ermite.example.org", up)
552	for _, name := range []string{"example.org", "other.example.org", "x.ermite.example.org", "ermite.example.org.evil.test", "ermite.example.or"} {
553		r := udpExchange(t, addr, query(9, name))
554		if string(r[len(r)-6:]) != "ANSWER" {
555			t.Fatalf("%s was not forwarded: %x", name, r)
556		}
557	}
558	if asked.Load() != 5 {
559		t.Fatalf("%d upstream queries, want 5", asked.Load())
560	}
561}
562
563func TestOverrideOverTCPAndDisabledByDefault(t *testing.T) {
564	up := fakeUpstream(t, answer)
565	_, addr := startOverride(t, "ermite.example.org", up)
566	tc, err := net.Dial("tcp", addr)
567	if err != nil {
568		t.Fatal(err)
569	}
570	defer func() { _ = tc.Close() }()
571	_ = tc.SetDeadline(time.Now().Add(5 * time.Second))
572	q := queryType(3, "ermite.example.org", 1, false)
573	_, _ = tc.Write(append([]byte{byte(len(q) >> 8), byte(len(q))}, q...))
574	var l [2]byte
575	if _, err := io.ReadFull(tc, l[:]); err != nil {
576		t.Fatal(err)
577	}
578	r := make([]byte, binary.BigEndian.Uint16(l[:]))
579	if _, err := io.ReadFull(tc, r); err != nil {
580		t.Fatal(err)
581	}
582	if binary.BigEndian.Uint16(r[6:]) != 1 || !strings.HasSuffix(string(r), "\x7f\x00\x00\x01") {
583		t.Fatalf("tcp: %x", r)
584	}
585
586	// Off by default: the name is forwarded like any other.
587	_, addr2 := start(t, up)
588	r2 := udpExchange(t, addr2, query(4, "ermite.example.org"))
589	if string(r2[len(r2)-6:]) != "ANSWER" {
590		t.Fatalf("not forwarded without Override: %x", r2)
591	}
592}
593
594func TestOverrideIgnoresMalformedAndNonQueries(t *testing.T) {
595	f := New(func() []string { return nil }, 0)
596	f.Override = "ermite.example.org"
597	gw := netip.MustParseAddr("10.200.0.1")
598	good := queryType(1, "ermite.example.org", 1, false)
599	if f.overrideAnswer(gw, good) == nil {
600		t.Fatal("control query not answered")
601	}
602	cut := good[:len(good)-3]
603	chaos := queryType(1, "ermite.example.org", 1, false)
604	binary.BigEndian.PutUint16(chaos[len(chaos)-2:], 3) // class CH
605	resp := append([]byte(nil), good...)
606	resp[2] |= 0x80 // QR set: not a query
607	notify := append([]byte(nil), good...)
608	notify[2] |= 4 << 3 // opcode 4
609	ptr := append(append([]byte(nil), good[:hdrLen]...), 0xC0, 0x0C, 0, 1, 0, 1)
610	for name, q := range map[string][]byte{"truncated": cut, "class CH": chaos, "response": resp, "opcode": notify, "pointer": ptr, "short": good[:5]} {
611		if f.overrideAnswer(gw, q) != nil {
612			t.Errorf("%s was answered locally", name)
613		}
614	}
615}
616
617// bigAnswer returns an answer to q padded to size bytes, with TC set if tc.
618func bigAnswer(q []byte, size int, tc bool) []byte {
619	r := answer(q)
620	if tc {
621		r[2] |= 0x02
622	}
623	for len(r) < size {
624		r = append(r, 'x')
625	}
626	return r
627}
628
629func TestLargeAnswerNonEDNSClientGetsTC(t *testing.T) {
630	up := fakeUpstream(t, func(q []byte) []byte { return bigAnswer(q, 700, false) })
631	_, addr := start(t, up)
632	q := queryType(0x3333, "storage.googleapis.com", 1, false)
633	resp := udpExchange(t, addr, q)
634	if resp[2]&0x02 == 0 {
635		t.Fatalf("TC not set: %x", resp)
636	}
637	if len(resp) != len(q) || binary.BigEndian.Uint16(resp[4:]) != 1 || binary.BigEndian.Uint16(resp[6:]) != 0 {
638		t.Fatalf("want header+question only, got %x", resp)
639	}
640	if resp[0] != q[0] || resp[1] != q[1] {
641		t.Fatal("ID changed")
642	}
643}
644
645func TestLargeAnswerEDNSClientGetsFullAnswer(t *testing.T) {
646	up := fakeUpstream(t, func(q []byte) []byte { return bigAnswer(q, 700, false) })
647	_, addr := start(t, up)
648	// queryType advertises 0x1000 (4096) bytes.
649	resp := udpExchange(t, addr, queryType(0x4444, "storage.googleapis.com", 1, true))
650	if len(resp) != 700 || resp[2]&0x02 != 0 {
651		t.Fatalf("want the full 700-byte answer, got len %d tc=%d", len(resp), resp[2]&0x02)
652	}
653}
654
655// withOPT appends an OPT record (root name, type 41) with the given class and
656// RDLEN to a query and bumps ARCOUNT.
657func withOPT(q []byte, size, rdlen uint16, body int) []byte {
658	q = append([]byte(nil), q...)
659	q[11] = 1
660	q = append(q, 0, 0, 41)
661	q = binary.BigEndian.AppendUint16(q, size)
662	q = append(q, 0, 0, 0, 0)
663	q = binary.BigEndian.AppendUint16(q, rdlen)
664	return append(q, make([]byte, body)...)
665}
666
667func TestClientUDPSize(t *testing.T) {
668	plain := query(1, "a.b")
669	bigQD := append([]byte(nil), plain[:hdrLen]...)
670	bigQD[4], bigQD[5] = 0xff, 0xff
671	bigQD = append(bigQD, 1, 'a', 0, 0, 1, 0, 1)
672	ptrQ := append([]byte(nil), plain[:hdrLen]...)
673	ptrQ = append(ptrQ, 0xc0, hdrLen, 0, 1, 0, 1) // compression pointer in question
674	tests := []struct {
675		name string
676		q    []byte
677		want int
678	}{
679		{"no edns", plain, 512},
680		{"edns 1232", withOPT(plain, 1232, 0, 0), 1232},
681		{"edns below 512", withOPT(plain, 100, 0, 0), 512},
682		{"edns above cap", withOPT(plain, 65535, 0, 0), maxUDP},
683		{"edns 4096", withOPT(plain, 4096, 0, 0), 4096},
684		{"qdcount 65535 short body", bigQD, 512},
685		{"compression pointer in question", ptrQ, 512},
686		{"OPT rdlen overruns buffer", withOPT(plain, 4096, 500, 3), 4096},
687		{"truncated header", plain[:15], 512},
688		{"short", plain[:5], 512},
689	}
690	for _, tt := range tests {
691		t.Run(tt.name, func(t *testing.T) {
692			if got := clientUDPSize(tt.q); got != tt.want {
693				t.Fatalf("got %d, want %d", got, tt.want)
694			}
695		})
696	}
697}
698
699func TestTruncated(t *testing.T) {
700	q := queryType(7, "a.b", 1, false)
701	full := bigAnswer(q, 700, false)
702	full[7] = 5 // ANCOUNT
703	got := truncated(full)
704	if len(got) != len(q) || got[2]&0x02 == 0 || got[6] != 0 || got[7] != 0 || got[4] != 0 || got[5] != 1 {
705		t.Fatalf("bad truncated reply %x", got)
706	}
707	bad := append([]byte(nil), full...)
708	bad[4], bad[5] = 0xff, 0xff // QDCOUNT lies, question parse fails
709	got = truncated(bad)
710	if len(got) != hdrLen || got[2]&0x02 == 0 || got[4] != 0 || got[5] != 0 {
711		t.Fatalf("bad-question case: %x", got)
712	}
713	if truncated(full[:5]) != nil {
714		t.Fatal("short input must give nil")
715	}
716}
717
718func TestServfailAndOverrideRepliesAreNotTruncated(t *testing.T) {
719	dead := "127.0.0.1:1" // nothing listens: UDP and TCP both fail at once
720	_, addr := startOverride(t, "forge.test", dead)
721	resp := udpExchange(t, addr, queryType(9, "x.test", 1, false))
722	if len(resp) != hdrLen || resp[3]&0x0f != 2 || resp[2]&0x02 != 0 {
723		t.Fatalf("want a plain 12-byte SERVFAIL, got %x", resp)
724	}
725	resp = udpExchange(t, addr, queryType(10, "forge.test", 1, false))
726	if resp[2]&0x02 != 0 || resp[3]&0x0f != 0 || binary.BigEndian.Uint16(resp[6:]) != 1 {
727		t.Fatalf("want the override A answer, got %x", resp)
728	}
729}
730
731// fakeNet is an in-memory upstream network for Forwarder.dial. One Write on a
732// UDP conn is one datagram (net.Pipe delivers a Write to a single Read when the
733// reader's buffer is large enough). A handler returning nil never answers.
734type fakeNet struct {
735	udp, tcp           func(addr string, q []byte) []byte
736	udpCalls, tcpCalls atomic.Int64
737}
738
739func (n *fakeNet) dial(ctx context.Context, network, addr string) (net.Conn, error) {
740	if err := ctx.Err(); err != nil { // like net.Dialer
741		return nil, err
742	}
743	c, s := net.Pipe()
744	switch network {
745	case "udp":
746		go func() {
747			defer func() { _ = s.Close() }()
748			buf := make([]byte, 4096)
749			for {
750				k, err := s.Read(buf)
751				if err != nil {
752					return
753				}
754				n.udpCalls.Add(1)
755				if r := n.udp(addr, append([]byte(nil), buf[:k]...)); r != nil {
756					if _, err := s.Write(r); err != nil {
757						return
758					}
759				}
760			}
761		}()
762	case "tcp":
763		go func() {
764			defer func() { _ = s.Close() }()
765			var l [2]byte
766			if _, err := io.ReadFull(s, l[:]); err != nil {
767				return
768			}
769			q := make([]byte, binary.BigEndian.Uint16(l[:]))
770			if _, err := io.ReadFull(s, q); err != nil {
771				return
772			}
773			n.tcpCalls.Add(1)
774			if r := n.tcp(addr, q); r != nil {
775				_, _ = s.Write(append(binary.BigEndian.AppendUint16(nil, uint16(len(r))), r...))
776			}
777			_, _ = io.Copy(io.Discard, s) // wait for the client to hang up
778		}()
779	default:
780		return nil, errors.New("unexpected network " + network)
781	}
782	return c, nil
783}
784
785// silent is a handler that never answers.
786func silent(string, []byte) []byte { return nil }
787
788// fakeForwarder returns a Forwarder with production timeouts whose upstreams
789// are served by n, and a listener for exchange.
790func fakeForwarder(n *fakeNet, ups ...string) (*Forwarder, *listener) {
791	f := New(func() []string { return ups }, 0)
792	f.dial = n.dial
793	return f, &listener{addr: netip.MustParseAddr("127.0.0.1")}
794}
795
796func isServfail(r []byte, id uint16) bool {
797	return len(r) == hdrLen && binary.BigEndian.Uint16(r) == id && r[3]&0x0f == 2
798}
799
800// The tests below run in a synctest bubble: the fake clock makes the real
801// production timeouts (1 s UDP, 3 s TCP, 6 s cap) instant and exact.
802
803func TestExchangeSilentUDPFallsBackToTCPAfterUDPTimeout(t *testing.T) {
804	synctest.Test(t, func(t *testing.T) {
805		n := &fakeNet{udp: silent, tcp: func(_ string, q []byte) []byte { return answer(q) }}
806		f, l := fakeForwarder(n, "up:53")
807		start := time.Now()
808		resp := f.exchange(t.Context(), l, query(1, "storage.googleapis.com"), false)
809		if !strings.HasSuffix(string(resp), "ANSWER") {
810			t.Fatalf("want the TCP answer, got %x", resp)
811		}
812		if got := time.Since(start); got != UDPAttemptTimeout {
813			t.Fatalf("answered after %v, want %v", got, UDPAttemptTimeout)
814		}
815		if n.udpCalls.Load() != 1 || n.tcpCalls.Load() != 1 {
816			t.Fatalf("udp=%d tcp=%d, want 1 and 1", n.udpCalls.Load(), n.tcpCalls.Load())
817		}
818	})
819}
820
821func TestExchangeTruncatedUDPRetriedOverTCP(t *testing.T) {
822	synctest.Test(t, func(t *testing.T) {
823		n := &fakeNet{
824			udp: func(_ string, q []byte) []byte { return bigAnswer(q, 0, true) },
825			tcp: func(_ string, q []byte) []byte { return bigAnswer(q, 700, false) },
826		}
827		f, l := fakeForwarder(n, "up:53")
828		start := time.Now()
829		resp := f.exchange(t.Context(), l, query(2, "example.com"), false)
830		if len(resp) != 700 || resp[2]&0x02 != 0 {
831			t.Fatalf("want the full TCP answer, got len %d tc=%d", len(resp), resp[2]&0x02)
832		}
833		if time.Since(start) != 0 {
834			t.Fatalf("took %v, want no waiting", time.Since(start))
835		}
836	})
837}
838
839func TestExchangeKeepsTruncatedReplyWhenTCPFails(t *testing.T) {
840	synctest.Test(t, func(t *testing.T) {
841		n := &fakeNet{udp: func(_ string, q []byte) []byte { return bigAnswer(q, 0, true) }, tcp: silent}
842		f, l := fakeForwarder(n, "up:53")
843		resp := f.exchange(t.Context(), l, query(3, "example.com"), false)
844		if len(resp) < hdrLen || resp[2]&0x02 == 0 {
845			t.Fatalf("want the TC=1 reply, got %x", resp)
846		}
847	})
848}
849
850func TestExchangeSecondUpstreamNotStarved(t *testing.T) {
851	synctest.Test(t, func(t *testing.T) {
852		n := &fakeNet{
853			udp: func(a string, q []byte) []byte {
854				if a == "dead:53" {
855					return nil
856				}
857				return answer(q)
858			},
859			tcp: func(a string, q []byte) []byte {
860				if a == "dead:53" {
861					return nil
862				}
863				return answer(q)
864			},
865		}
866		f, l := fakeForwarder(n, "dead:53", "live:53")
867		start := time.Now()
868		resp := f.exchange(t.Context(), l, query(4, "a.test"), false)
869		if !strings.HasSuffix(string(resp), "ANSWER") {
870			t.Fatalf("want the second upstream's answer, got %x", resp)
871		}
872		if got, want := time.Since(start), UDPAttemptTimeout+UpstreamTimeout; got != want {
873			t.Fatalf("took %v, want %v", got, want)
874		}
875	})
876}
877
878func TestExchangeMismatchedUpstreamIDIgnored(t *testing.T) {
879	synctest.Test(t, func(t *testing.T) {
880		wrong := func(_ string, q []byte) []byte {
881			r := answer(q)
882			r[0] ^= 0xff
883			return r
884		}
885		f, l := fakeForwarder(&fakeNet{udp: wrong, tcp: wrong}, "up:53")
886		start := time.Now()
887		resp := f.exchange(t.Context(), l, query(5, "a.test"), false)
888		if !isServfail(resp, 5) {
889			t.Fatalf("want SERVFAIL, got %x", resp)
890		}
891		// UDP keeps listening for the right ID until its timeout; the TCP
892		// reply is read once and rejected.
893		if got := time.Since(start); got != UDPAttemptTimeout {
894			t.Fatalf("took %v, want %v", got, UDPAttemptTimeout)
895		}
896	})
897}
898
899func TestExchangeCappedAtMaxExchange(t *testing.T) {
900	synctest.Test(t, func(t *testing.T) {
901		n := &fakeNet{udp: silent, tcp: silent}
902		f, l := fakeForwarder(n, "a:53", "b:53", "c:53")
903		start := time.Now()
904		resp := f.exchange(t.Context(), l, query(6, "a.test"), false)
905		if !isServfail(resp, 6) {
906			t.Fatalf("want SERVFAIL, got %x", resp)
907		}
908		if got := time.Since(start); got != MaxExchange {
909			t.Fatalf("took %v, want %v", got, MaxExchange)
910		}
911		// a: 1 s UDP + 3 s TCP; b: 1 s UDP + 1 s TCP (cut by the cap); c: skipped.
912		if n.udpCalls.Load() != 2 || n.tcpCalls.Load() != 2 {
913			t.Fatalf("udp=%d tcp=%d, want 2 and 2", n.udpCalls.Load(), n.tcpCalls.Load())
914		}
915	})
916}
917
918func TestExchangeTCPClientUsesTCPOnly(t *testing.T) {
919	synctest.Test(t, func(t *testing.T) {
920		n := &fakeNet{udp: silent, tcp: func(_ string, q []byte) []byte { return answer(q) }}
921		f, l := fakeForwarder(n, "up:53")
922		resp := f.exchange(t.Context(), l, query(7, "a.test"), true)
923		if !strings.HasSuffix(string(resp), "ANSWER") || n.udpCalls.Load() != 0 {
924			t.Fatalf("resp=%x udp=%d", resp, n.udpCalls.Load())
925		}
926	})
927}
928
929func TestExchangeCancelledReturnsNil(t *testing.T) {
930	synctest.Test(t, func(t *testing.T) {
931		f, l := fakeForwarder(&fakeNet{udp: silent, tcp: silent}, "up:53")
932		ctx, cancel := context.WithCancel(t.Context())
933		time.AfterFunc(500*time.Millisecond, cancel)
934		start := time.Now()
935		if resp := f.exchange(ctx, l, query(8, "a.test"), false); resp != nil {
936			t.Fatalf("want nil on cancellation, got %x", resp)
937		}
938		// Cancellation is noticed at the next attempt boundary: the blocked UDP
939		// read runs to its own deadline, then no TCP attempt is started.
940		if got := time.Since(start); got != UDPAttemptTimeout {
941			t.Fatalf("returned after %v, want %v", got, UDPAttemptTimeout)
942		}
943	})
944}
945
946// End to end through real client-facing sockets; the upstream is in memory.
947func TestUDPClientGetsTCPAnswerWhenUpstreamUDPSilent(t *testing.T) {
948	n := &fakeNet{udp: silent, tcp: func(_ string, q []byte) []byte { return answer(q) }}
949	_, addr := startWith(t, func(f *Forwarder) {
950		f.dial = n.dial
951		f.udpTimeout = 50 * time.Millisecond
952	}, "up:53")
953	resp := udpExchange(t, addr, query(0x1111, "storage.googleapis.com"))
954	if !strings.HasSuffix(string(resp), "ANSWER") || resp[3]&0x0f != 0 {
955		t.Fatalf("want the TCP answer, got %x", resp)
956	}
957}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 4

Tags 11

↑↓ moveenter openesc close