v0.1.0 cbb8ad6

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	"time"
20)
21
22// query builds a minimal query for name with the given ID.
23func query(id uint16, name string) []byte {
24	b := make([]byte, hdrLen)
25	binary.BigEndian.PutUint16(b, id)
26	b[2] = 0x01 // RD
27	b[5] = 1    // QDCOUNT
28	for _, l := range strings.Split(name, ".") {
29		b = append(b, byte(len(l)))
30		b = append(b, l...)
31	}
32	return append(b, 0, 0, 1, 0, 1) // root, A, IN
33}
34
35// answer turns a query into a response carrying a recognisable payload.
36func answer(q []byte) []byte {
37	r := append([]byte(nil), q...)
38	r[2] |= 0x80
39	return append(r, "ANSWER"...)
40}
41
42// fakeUpstream serves UDP and TCP on one port of 127.0.0.1 and returns it.
43func fakeUpstream(t *testing.T, handle func(q []byte) []byte) string {
44	t.Helper()
45	pc, err := net.ListenPacket("udp", "127.0.0.1:0")
46	if err != nil {
47		t.Skip("no loopback:", err)
48	}
49	addr := pc.LocalAddr().String()
50	tl, err := net.Listen("tcp", addr)
51	if err != nil {
52		t.Fatal(err)
53	}
54	t.Cleanup(func() { _ = pc.Close(); _ = tl.Close() })
55	go func() {
56		buf := make([]byte, 4096)
57		for {
58			n, from, err := pc.ReadFrom(buf)
59			if err != nil {
60				return
61			}
62			if r := handle(append([]byte(nil), buf[:n]...)); r != nil {
63				_, _ = pc.WriteTo(r, from)
64			}
65		}
66	}()
67	go func() {
68		for {
69			c, err := tl.Accept()
70			if err != nil {
71				return
72			}
73			go func() {
74				defer func() { _ = c.Close() }()
75				var l [2]byte
76				if _, err := io.ReadFull(c, l[:]); err != nil {
77					return
78				}
79				q := make([]byte, binary.BigEndian.Uint16(l[:]))
80				if _, err := io.ReadFull(c, q); err != nil {
81					return
82				}
83				r := handle(q)
84				out := make([]byte, 2+len(r))
85				binary.BigEndian.PutUint16(out, uint16(len(r)))
86				copy(out[2:], r)
87				_, _ = c.Write(out)
88			}()
89		}
90	}()
91	return addr
92}
93
94func start(t *testing.T, ups ...string) (*Forwarder, string) {
95	t.Helper()
96	f := New(func() []string { return ups }, 0)
97	ip := netip.MustParseAddr("127.0.0.1")
98	if err := f.Listen(context.Background(), ip); err != nil {
99		t.Fatal(err)
100	}
101	t.Cleanup(f.CloseAll)
102	a, _ := f.Addr(ip)
103	return f, a.String()
104}
105
106func TestUDPAndTCPForwarding(t *testing.T) {
107	up := fakeUpstream(t, answer)
108	_, addr := start(t, up)
109
110	c, err := net.Dial("udp", addr)
111	if err != nil {
112		t.Fatal(err)
113	}
114	defer func() { _ = c.Close() }()
115	_ = c.SetDeadline(time.Now().Add(5 * time.Second))
116	q := query(0x1234, "example.test")
117	_, _ = c.Write(q)
118	buf := make([]byte, 512)
119	n, err := c.Read(buf)
120	if err != nil {
121		t.Fatal(err)
122	}
123	if string(buf[n-6:n]) != "ANSWER" || binary.BigEndian.Uint16(buf) != 0x1234 {
124		t.Fatalf("udp answer: %x", buf[:n])
125	}
126
127	tc, err := net.Dial("tcp", addr)
128	if err != nil {
129		t.Fatal(err)
130	}
131	defer func() { _ = tc.Close() }()
132	_ = tc.SetDeadline(time.Now().Add(5 * time.Second))
133	// Two queries on one connection.
134	for _, id := range []uint16{7, 8} {
135		q := query(id, "tcp.test")
136		framed := append([]byte{byte(len(q) >> 8), byte(len(q))}, q...)
137		_, _ = tc.Write(framed)
138		var l [2]byte
139		if _, err := io.ReadFull(tc, l[:]); err != nil {
140			t.Fatal(err)
141		}
142		r := make([]byte, binary.BigEndian.Uint16(l[:]))
143		if _, err := io.ReadFull(tc, r); err != nil {
144			t.Fatal(err)
145		}
146		if binary.BigEndian.Uint16(r) != id || string(r[len(r)-6:]) != "ANSWER" {
147			t.Fatalf("tcp answer for %d: %x", id, r)
148		}
149	}
150}
151
152func TestFailoverToSecondUpstream(t *testing.T) {
153	dead := "127.0.0.1:1" // nothing listens: UDP read fails with ICMP refused
154	live := fakeUpstream(t, answer)
155	_, addr := start(t, dead, live)
156	c, _ := net.Dial("udp", addr)
157	defer func() { _ = c.Close() }()
158	_ = c.SetDeadline(time.Now().Add(8 * time.Second))
159	_, _ = c.Write(query(1, "a.test"))
160	buf := make([]byte, 512)
161	n, err := c.Read(buf)
162	if err != nil || string(buf[n-6:n]) != "ANSWER" {
163		t.Fatalf("n=%d err=%v", n, err)
164	}
165}
166
167func TestServfailWhenNoUpstreamAnswers(t *testing.T) {
168	_, addr := start(t) // no upstream at all
169	c, _ := net.Dial("udp", addr)
170	defer func() { _ = c.Close() }()
171	_ = c.SetDeadline(time.Now().Add(5 * time.Second))
172	_, _ = c.Write(query(0xbeef, "a.test"))
173	buf := make([]byte, 512)
174	n, err := c.Read(buf)
175	if err != nil {
176		t.Fatal(err)
177	}
178	if n != hdrLen || binary.BigEndian.Uint16(buf) != 0xbeef || buf[3]&0x0f != 2 || buf[2]&0x80 == 0 {
179		t.Fatalf("not a SERVFAIL: %x", buf[:n])
180	}
181}
182
183func TestMismatchedUpstreamIDIgnored(t *testing.T) {
184	// Upstream answers with a wrong ID: treated as no answer, client gets SERVFAIL.
185	up := fakeUpstream(t, func(q []byte) []byte {
186		r := answer(q)
187		r[0] ^= 0xff
188		return r
189	})
190	f, addr := start(t, up)
191	_ = f
192	c, _ := net.Dial("udp", addr)
193	defer func() { _ = c.Close() }()
194	_ = c.SetDeadline(time.Now().Add(2*UpstreamTimeout + 2*time.Second))
195	_, _ = c.Write(query(5, "a.test"))
196	buf := make([]byte, 512)
197	n, err := c.Read(buf)
198	if err != nil {
199		t.Fatal(err)
200	}
201	if buf[3]&0x0f != 2 {
202		t.Fatalf("want SERVFAIL, got %x", buf[:n])
203	}
204}
205
206func TestShortPacketsIgnored(t *testing.T) {
207	up := fakeUpstream(t, answer)
208	_, addr := start(t, up)
209	c, _ := net.Dial("udp", addr)
210	defer func() { _ = c.Close() }()
211	_, _ = c.Write([]byte{1, 2, 3}) // dropped
212	_, _ = c.Write(query(9, "a.test"))
213	_ = c.SetDeadline(time.Now().Add(5 * time.Second))
214	buf := make([]byte, 512)
215	n, err := c.Read(buf)
216	if err != nil || binary.BigEndian.Uint16(buf) != 9 {
217		t.Fatalf("n=%d err=%v", n, err)
218	}
219}
220
221func TestListenTwiceAndClose(t *testing.T) {
222	f := New(func() []string { return nil }, 0)
223	ip := netip.MustParseAddr("127.0.0.1")
224	if err := f.Listen(context.Background(), ip); err != nil {
225		t.Skip(err)
226	}
227	if err := f.Listen(context.Background(), ip); err == nil {
228		t.Fatal("second Listen on the same address must fail")
229	}
230	a, _ := f.Addr(ip)
231	f.Close(ip)
232	f.Close(ip) // idempotent
233	if _, ok := f.Addr(ip); ok {
234		t.Fatal("still listening after Close")
235	}
236	if pc, err := net.ListenPacket("udp", a.String()); err != nil {
237		t.Fatalf("port not released: %v", err)
238	} else {
239		_ = pc.Close()
240	}
241}
242
243func TestServfailShape(t *testing.T) {
244	if servfail([]byte{1}) != nil {
245		t.Fatal("short query must give nil")
246	}
247	q := query(0xabcd, "x.test")
248	q[2] = 0x79 // opcode bits + RD + AA
249	r := servfail(q)
250	if r[0] != 0xab || r[1] != 0xcd || r[2]&0x80 == 0 || r[3] != 0x82 {
251		t.Fatalf("bad servfail %x", r)
252	}
253}
254
255func TestParseResolvConf(t *testing.T) {
256	in := `# comment
257nameserver 127.0.0.53
258nameserver 127.0.0.53
259nameserver 192.0.2.1 # trailing
260nameserver 2001:db8::1
261nameserver fe80::1%eth0
262nameserver 0.0.0.0
263nameserver not-an-ip
264search example.test
265options edns0
266nameserver
267`
268	got := ParseResolvConf(strings.NewReader(in))
269	want := []string{"127.0.0.53:53", "192.0.2.1:53", "[2001:db8::1]:53", "[fe80::1%eth0]:53"}
270	if !reflect.DeepEqual(got, want) {
271		t.Fatalf("got %v want %v", got, want)
272	}
273}
274
275func TestResolversReload(t *testing.T) {
276	p := filepath.Join(t.TempDir(), "resolv.conf")
277	if err := os.WriteFile(p, []byte("nameserver 192.0.2.1\n"), 0o644); err != nil {
278		t.Fatal(err)
279	}
280	r := NewResolvers(p)
281	if got := r.Addrs(); !reflect.DeepEqual(got, []string{"192.0.2.1:53"}) {
282		t.Fatal(got)
283	}
284	if err := os.WriteFile(p, []byte("nameserver 192.0.2.99\nnameserver 192.0.2.1\n"), 0o644); err != nil {
285		t.Fatal(err)
286	}
287	later := time.Now().Add(2 * time.Second)
288	_ = os.Chtimes(p, later, later)
289	if got := r.Addrs(); !reflect.DeepEqual(got, []string{"192.0.2.99:53", "192.0.2.1:53"}) {
290		t.Fatal(got)
291	}
292	// Unreadable file keeps the last known list.
293	_ = os.Remove(p)
294	if got := r.Addrs(); len(got) != 2 {
295		t.Fatalf("lost the last known list: %v", got)
296	}
297	if got := NewResolvers(filepath.Join(t.TempDir(), "missing")).Addrs(); len(got) != 0 {
298		t.Fatal(got)
299	}
300}
301
302// S2: a listener holds at most MaxTCPConns connections; extra ones are closed
303// at once, and a free slot is usable again after a close.
304func TestTCPConnectionCapPerListener(t *testing.T) {
305	up := fakeUpstream(t, answer)
306	_, addr := start(t, up)
307	var held []net.Conn
308	for range MaxTCPConns {
309		c, err := net.Dial("tcp", addr)
310		if err != nil {
311			t.Fatal(err)
312		}
313		held = append(held, c)
314		defer func() { _ = c.Close() }()
315	}
316	// Let the accept loop register all of them.
317	time.Sleep(200 * time.Millisecond)
318	extra, err := net.Dial("tcp", addr)
319	if err != nil {
320		t.Fatal(err)
321	}
322	defer func() { _ = extra.Close() }()
323	_ = extra.SetReadDeadline(time.Now().Add(3 * time.Second))
324	if _, err := extra.Read(make([]byte, 1)); err != io.EOF && !errors.Is(err, syscall.ECONNRESET) {
325		t.Fatalf("the connection over the cap must be closed by the server, got %v", err)
326	}
327	// Free one slot: a new connection is served.
328	_ = held[0].Close()
329	time.Sleep(200 * time.Millisecond)
330	c, err := net.Dial("tcp", addr)
331	if err != nil {
332		t.Fatal(err)
333	}
334	defer func() { _ = c.Close() }()
335	_ = c.SetDeadline(time.Now().Add(5 * time.Second))
336	q := query(3, "slot.test")
337	_, _ = c.Write(append([]byte{byte(len(q) >> 8), byte(len(q))}, q...))
338	var l [2]byte
339	if _, err := io.ReadFull(c, l[:]); err != nil {
340		t.Fatalf("a freed slot must serve again: %v", err)
341	}
342}
343
344// S2: the lifetime is absolute from accept: a client that keeps the connection
345// alive by trickling bytes still loses it.
346func TestTCPAbsoluteDeadline(t *testing.T) {
347	up := fakeUpstream(t, answer)
348	f := New(func() []string { return []string{up} }, 0)
349	f.Lifetime = 600 * time.Millisecond
350	ip := netip.MustParseAddr("127.0.0.1")
351	if err := f.Listen(context.Background(), ip); err != nil {
352		t.Skip(err)
353	}
354	t.Cleanup(f.CloseAll)
355	la, _ := f.Addr(ip)
356	addr := la.String()
357	c, err := net.Dial("tcp", addr)
358	if err != nil {
359		t.Fatal(err)
360	}
361	defer func() { _ = c.Close() }()
362	start := time.Now()
363	closed := make(chan struct{})
364	go func() {
365		_, _ = io.Copy(io.Discard, c) // returns when the server closes
366		close(closed)
367	}()
368	// Trickle one byte every 100 ms, never finishing a frame: only an idle
369	// timeout (10 s) would let this live; the absolute deadline must not.
370	go func() {
371		for i := 0; i < 40; i++ {
372			if _, err := c.Write([]byte{0}); err != nil {
373				return
374			}
375			time.Sleep(100 * time.Millisecond)
376		}
377	}()
378	select {
379	case <-closed:
380		if d := time.Since(start); d > 3*time.Second {
381			t.Fatalf("closed after %v, want about %v", d, f.Lifetime)
382		}
383	case <-time.After(5 * time.Second):
384		t.Fatal("a trickling client kept its connection past the absolute deadline")
385	}
386}
387
388// S2/S9: a saturated listener drops UDP queries instead of blocking its read
389// loop, and another listener (another guest) keeps answering.
390func TestUDPSaturationDropsAndIsolatesListeners(t *testing.T) {
391	release := make(chan struct{})
392	slow := fakeUpstream(t, func(q []byte) []byte { <-release; return answer(q) })
393	fast := fakeUpstream(t, answer)
394	var useSlow atomic.Bool
395	useSlow.Store(true)
396	f := New(func() []string {
397		if useSlow.Load() {
398			return []string{slow}
399		}
400		return []string{fast}
401	}, 0)
402	a1, a2 := netip.MustParseAddr("127.0.0.1"), netip.MustParseAddr("127.0.0.2")
403	if err := f.Listen(context.Background(), a1); err != nil {
404		t.Skip(err)
405	}
406	if err := f.Listen(context.Background(), a2); err != nil {
407		t.Skip(err)
408	}
409	t.Cleanup(f.CloseAll)
410	t.Cleanup(func() { close(release) })
411	l1, _ := f.Addr(a1)
412	flood, err := net.Dial("udp", l1.String())
413	if err != nil {
414		t.Fatal(err)
415	}
416	defer func() { _ = flood.Close() }()
417	for i := 0; i < MaxInflight*3; i++ {
418		_, _ = flood.Write(query(uint16(i), "flood.test"))
419	}
420	deadline := time.Now().Add(3 * time.Second)
421	for f.Dropped(a1) == 0 && time.Now().Before(deadline) {
422		time.Sleep(20 * time.Millisecond)
423	}
424	if f.Dropped(a1) == 0 {
425		t.Fatal("a saturated listener must drop queries")
426	}
427	// The other listener is unaffected: point the resolver at the fast upstream.
428	useSlow.Store(false)
429	l2, _ := f.Addr(a2)
430	c, err := net.Dial("udp", l2.String())
431	if err != nil {
432		t.Fatal(err)
433	}
434	defer func() { _ = c.Close() }()
435	_ = c.SetDeadline(time.Now().Add(4 * time.Second))
436	_, _ = c.Write(query(77, "other-guest.test"))
437	buf := make([]byte, 512)
438	n, err := c.Read(buf)
439	if err != nil || string(buf[n-6:n]) != "ANSWER" {
440		t.Fatalf("another listener must not be starved by the flood: n=%d err=%v", n, err)
441	}
442}
443
444// By default Listen fails fast on an address the host does not own (Docker and
445// the VM driver have configured it by then: a missing one is an error).
446func TestListenOnAMissingAddressFailsByDefault(t *testing.T) {
447	f := New(func() []string { return nil }, 0)
448	t.Cleanup(f.CloseAll)
449	ip := netip.MustParseAddr("203.0.113.77") // TEST-NET-3: never configured here
450	if err := f.Listen(context.Background(), ip); err == nil {
451		t.Fatal("listening on an address the host does not own must fail without FreeBind")
452	}
453}
454
455// With FreeBind (podman's netavark creates the bridge address only when the
456// first container starts) Listen binds an address the host does not own yet.
457func TestFreeBindListensOnAnAddressTheHostDoesNotOwnYet(t *testing.T) {
458	f := New(func() []string { return nil }, 0)
459	f.FreeBind = true
460	t.Cleanup(f.CloseAll)
461	ip := netip.MustParseAddr("203.0.113.77")
462	if err := f.Listen(context.Background(), ip); err != nil {
463		t.Fatalf("FreeBind: %v", err)
464	}
465	if _, ok := f.Addr(ip); !ok {
466		t.Fatal("no listener")
467	}
468}

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