1// Copyright 2013 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5//go:build unix
6
7package net
8
9import (
10	"errors"
11	"io/fs"
12	"os"
13	"reflect"
14	"strings"
15	"testing"
16	"time"
17)
18
19var dnsReadConfigTests = []struct {
20	name string
21	want *dnsConfig
22}{
23	{
24		name: "testdata/resolv.conf",
25		want: &dnsConfig{
26			servers:    []string{"8.8.8.8:53", "[2001:4860:4860::8888]:53", "[fe80::1%lo0]:53"},
27			search:     []string{"localdomain."},
28			ndots:      5,
29			timeout:    10 * time.Second,
30			attempts:   3,
31			rotate:     true,
32			unknownOpt: true, // the "options attempts 3" line
33		},
34	},
35	{
36		name: "testdata/domain-resolv.conf",
37		want: &dnsConfig{
38			servers:  []string{"8.8.8.8:53"},
39			search:   []string{"localdomain."},
40			ndots:    1,
41			timeout:  5 * time.Second,
42			attempts: 2,
43		},
44	},
45	{
46		name: "testdata/search-resolv.conf",
47		want: &dnsConfig{
48			servers:  []string{"8.8.8.8:53"},
49			search:   []string{"test.", "invalid."},
50			ndots:    1,
51			timeout:  5 * time.Second,
52			attempts: 2,
53		},
54	},
55	{
56		name: "testdata/search-single-dot-resolv.conf",
57		want: &dnsConfig{
58			servers:  []string{"8.8.8.8:53"},
59			search:   []string{},
60			ndots:    1,
61			timeout:  5 * time.Second,
62			attempts: 2,
63		},
64	},
65	{
66		name: "testdata/empty-resolv.conf",
67		want: &dnsConfig{
68			servers:  defaultNS,
69			ndots:    1,
70			timeout:  5 * time.Second,
71			attempts: 2,
72			search:   []string{"domain.local."},
73		},
74	},
75	{
76		name: "testdata/invalid-ndots-resolv.conf",
77		want: &dnsConfig{
78			servers:  defaultNS,
79			ndots:    0,
80			timeout:  5 * time.Second,
81			attempts: 2,
82			search:   []string{"domain.local."},
83		},
84	},
85	{
86		name: "testdata/large-ndots-resolv.conf",
87		want: &dnsConfig{
88			servers:  defaultNS,
89			ndots:    15,
90			timeout:  5 * time.Second,
91			attempts: 2,
92			search:   []string{"domain.local."},
93		},
94	},
95	{
96		name: "testdata/negative-ndots-resolv.conf",
97		want: &dnsConfig{
98			servers:  defaultNS,
99			ndots:    0,
100			timeout:  5 * time.Second,
101			attempts: 2,
102			search:   []string{"domain.local."},
103		},
104	},
105	{
106		name: "testdata/openbsd-resolv.conf",
107		want: &dnsConfig{
108			ndots:    1,
109			timeout:  5 * time.Second,
110			attempts: 2,
111			lookup:   []string{"file", "bind"},
112			servers:  []string{"169.254.169.254:53", "10.240.0.1:53"},
113			search:   []string{"c.symbolic-datum-552.internal."},
114		},
115	},
116	{
117		name: "testdata/single-request-resolv.conf",
118		want: &dnsConfig{
119			servers:       defaultNS,
120			ndots:         1,
121			singleRequest: true,
122			timeout:       5 * time.Second,
123			attempts:      2,
124			search:        []string{"domain.local."},
125		},
126	},
127	{
128		name: "testdata/single-request-reopen-resolv.conf",
129		want: &dnsConfig{
130			servers:       defaultNS,
131			ndots:         1,
132			singleRequest: true,
133			timeout:       5 * time.Second,
134			attempts:      2,
135			search:        []string{"domain.local."},
136		},
137	},
138	{
139		name: "testdata/linux-use-vc-resolv.conf",
140		want: &dnsConfig{
141			servers:  defaultNS,
142			ndots:    1,
143			useTCP:   true,
144			timeout:  5 * time.Second,
145			attempts: 2,
146			search:   []string{"domain.local."},
147		},
148	},
149	{
150		name: "testdata/freebsd-usevc-resolv.conf",
151		want: &dnsConfig{
152			servers:  defaultNS,
153			ndots:    1,
154			useTCP:   true,
155			timeout:  5 * time.Second,
156			attempts: 2,
157			search:   []string{"domain.local."},
158		},
159	},
160	{
161		name: "testdata/openbsd-tcp-resolv.conf",
162		want: &dnsConfig{
163			servers:  defaultNS,
164			ndots:    1,
165			useTCP:   true,
166			timeout:  5 * time.Second,
167			attempts: 2,
168			search:   []string{"domain.local."},
169		},
170	},
171}
172
173func TestDNSReadConfig(t *testing.T) {
174	origGetHostname := getHostname
175	defer func() { getHostname = origGetHostname }()
176	getHostname = func() (string, error) { return "host.domain.local", nil }
177
178	for _, tt := range dnsReadConfigTests {
179		want := *tt.want
180		if len(want.search) == 0 {
181			want.search = dnsDefaultSearch()
182		}
183		conf := dnsReadConfig(tt.name)
184		if conf.err != nil {
185			t.Fatal(conf.err)
186		}
187		conf.mtime = time.Time{}
188		if !reflect.DeepEqual(conf, &want) {
189			t.Errorf("%s:\ngot: %+v\nwant: %+v", tt.name, conf, want)
190		}
191	}
192}
193
194func TestDNSReadMissingFile(t *testing.T) {
195	origGetHostname := getHostname
196	defer func() { getHostname = origGetHostname }()
197	getHostname = func() (string, error) { return "host.domain.local", nil }
198
199	conf := dnsReadConfig("a-nonexistent-file")
200	if !os.IsNotExist(conf.err) {
201		t.Errorf("missing resolv.conf:\ngot: %v\nwant: %v", conf.err, fs.ErrNotExist)
202	}
203	conf.err = nil
204	want := &dnsConfig{
205		servers:  defaultNS,
206		ndots:    1,
207		timeout:  5 * time.Second,
208		attempts: 2,
209		search:   []string{"domain.local."},
210	}
211	if !reflect.DeepEqual(conf, want) {
212		t.Errorf("missing resolv.conf:\ngot: %+v\nwant: %+v", conf, want)
213	}
214}
215
216var dnsDefaultSearchTests = []struct {
217	name string
218	err  error
219	want []string
220}{
221	{
222		name: "host.long.domain.local",
223		want: []string{"long.domain.local."},
224	},
225	{
226		name: "host.local",
227		want: []string{"local."},
228	},
229	{
230		name: "host",
231		want: nil,
232	},
233	{
234		name: "host.domain.local",
235		err:  errors.New("errored"),
236		want: nil,
237	},
238	{
239		// ensures we don't return []string{""}
240		// which causes duplicate lookups
241		name: "foo.",
242		want: nil,
243	},
244}
245
246func TestDNSDefaultSearch(t *testing.T) {
247	origGetHostname := getHostname
248	defer func() { getHostname = origGetHostname }()
249
250	for _, tt := range dnsDefaultSearchTests {
251		getHostname = func() (string, error) { return tt.name, tt.err }
252		got := dnsDefaultSearch()
253		if !reflect.DeepEqual(got, tt.want) {
254			t.Errorf("dnsDefaultSearch with hostname %q and error %+v = %q, wanted %q", tt.name, tt.err, got, tt.want)
255		}
256	}
257}
258
259func TestDNSNameLength(t *testing.T) {
260	origGetHostname := getHostname
261	defer func() { getHostname = origGetHostname }()
262	getHostname = func() (string, error) { return "host.domain.local", nil }
263
264	var char63 = ""
265	for i := 0; i < 63; i++ {
266		char63 += "a"
267	}
268	longDomain := strings.Repeat(char63+".", 5) + "example"
269
270	for _, tt := range dnsReadConfigTests {
271		conf := dnsReadConfig(tt.name)
272		if conf.err != nil {
273			t.Fatal(conf.err)
274		}
275
276		suffixList := tt.want.search
277		if len(suffixList) == 0 {
278			suffixList = dnsDefaultSearch()
279		}
280
281		var shortestSuffix int
282		for _, suffix := range suffixList {
283			if shortestSuffix == 0 || len(suffix) < shortestSuffix {
284				shortestSuffix = len(suffix)
285			}
286		}
287
288		// Test a name that will be maximally long when prefixing the shortest
289		// suffix (accounting for the intervening dot).
290		longName := longDomain[len(longDomain)-254+1+shortestSuffix:]
291		if longName[0] == '.' || longName[1] == '.' {
292			longName = "aa." + longName[3:]
293		}
294		for _, fqdn := range conf.nameList(longName) {
295			if len(fqdn) > 254 {
296				t.Errorf("got %d; want less than or equal to 254", len(fqdn))
297			}
298		}
299
300		// Now test a name that's too long for suffixing.
301		unsuffixable := "a." + longName[1:]
302		unsuffixableResults := conf.nameList(unsuffixable)
303		if len(unsuffixableResults) != 1 {
304			t.Errorf("suffixed names %v; want []", unsuffixableResults[1:])
305		}
306
307		// Now test a name that's too long for DNS.
308		tooLong := "a." + longDomain
309		tooLongResults := conf.nameList(tooLong)
310		if tooLongResults != nil {
311			t.Errorf("suffixed names %v; want nil", tooLongResults)
312		}
313	}
314}
315