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// +build darwin dragonfly freebsd linux netbsd openbsd solaris
6
7package net
8
9import (
10	"fmt"
11	"io/ioutil"
12	"os"
13	"path"
14	"reflect"
15	"strings"
16	"sync"
17	"testing"
18	"time"
19)
20
21var dnsTransportFallbackTests = []struct {
22	server  string
23	name    string
24	qtype   uint16
25	timeout int
26	rcode   int
27}{
28	// Querying "com." with qtype=255 usually makes an answer
29	// which requires more than 512 bytes.
30	{"8.8.8.8:53", "com.", dnsTypeALL, 2, dnsRcodeSuccess},
31	{"8.8.4.4:53", "com.", dnsTypeALL, 4, dnsRcodeSuccess},
32}
33
34func TestDNSTransportFallback(t *testing.T) {
35	if testing.Short() || !*testExternal {
36		t.Skip("avoid external network")
37	}
38
39	for _, tt := range dnsTransportFallbackTests {
40		timeout := time.Duration(tt.timeout) * time.Second
41		msg, err := exchange(tt.server, tt.name, tt.qtype, timeout)
42		if err != nil {
43			t.Error(err)
44			continue
45		}
46		switch msg.rcode {
47		case tt.rcode, dnsRcodeServerFailure:
48		default:
49			t.Errorf("got %v from %v; want %v", msg.rcode, tt.server, tt.rcode)
50			continue
51		}
52	}
53}
54
55// See RFC 6761 for further information about the reserved, pseudo
56// domain names.
57var specialDomainNameTests = []struct {
58	name  string
59	qtype uint16
60	rcode int
61}{
62	// Name resolution APIs and libraries should not recognize the
63	// followings as special.
64	{"1.0.168.192.in-addr.arpa.", dnsTypePTR, dnsRcodeNameError},
65	{"test.", dnsTypeALL, dnsRcodeNameError},
66	{"example.com.", dnsTypeALL, dnsRcodeSuccess},
67
68	// Name resolution APIs and libraries should recognize the
69	// followings as special and should not send any queries.
70	// Though, we test those names here for verifying nagative
71	// answers at DNS query-response interaction level.
72	{"localhost.", dnsTypeALL, dnsRcodeNameError},
73	{"invalid.", dnsTypeALL, dnsRcodeNameError},
74}
75
76func TestSpecialDomainName(t *testing.T) {
77	if testing.Short() || !*testExternal {
78		t.Skip("avoid external network")
79	}
80
81	server := "8.8.8.8:53"
82	for _, tt := range specialDomainNameTests {
83		msg, err := exchange(server, tt.name, tt.qtype, 3*time.Second)
84		if err != nil {
85			t.Error(err)
86			continue
87		}
88		switch msg.rcode {
89		case tt.rcode, dnsRcodeServerFailure:
90		default:
91			t.Errorf("got %v from %v; want %v", msg.rcode, server, tt.rcode)
92			continue
93		}
94	}
95}
96
97type resolvConfTest struct {
98	dir  string
99	path string
100	*resolverConfig
101}
102
103func newResolvConfTest() (*resolvConfTest, error) {
104	dir, err := ioutil.TempDir("", "go-resolvconftest")
105	if err != nil {
106		return nil, err
107	}
108	conf := &resolvConfTest{
109		dir:            dir,
110		path:           path.Join(dir, "resolv.conf"),
111		resolverConfig: &resolvConf,
112	}
113	conf.initOnce.Do(conf.init)
114	return conf, nil
115}
116
117func (conf *resolvConfTest) writeAndUpdate(lines []string) error {
118	f, err := os.OpenFile(conf.path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
119	if err != nil {
120		return err
121	}
122	if _, err := f.WriteString(strings.Join(lines, "\n")); err != nil {
123		f.Close()
124		return err
125	}
126	f.Close()
127	if err := conf.forceUpdate(conf.path); err != nil {
128		return err
129	}
130	return nil
131}
132
133func (conf *resolvConfTest) forceUpdate(name string) error {
134	dnsConf := dnsReadConfig(name)
135	conf.mu.Lock()
136	conf.dnsConfig = dnsConf
137	conf.mu.Unlock()
138	for i := 0; i < 5; i++ {
139		if conf.tryAcquireSema() {
140			conf.lastChecked = time.Time{}
141			conf.releaseSema()
142			return nil
143		}
144	}
145	return fmt.Errorf("tryAcquireSema for %s failed", name)
146}
147
148func (conf *resolvConfTest) servers() []string {
149	conf.mu.RLock()
150	servers := conf.dnsConfig.servers
151	conf.mu.RUnlock()
152	return servers
153}
154
155func (conf *resolvConfTest) teardown() error {
156	err := conf.forceUpdate("/etc/resolv.conf")
157	os.RemoveAll(conf.dir)
158	return err
159}
160
161var updateResolvConfTests = []struct {
162	name    string   // query name
163	lines   []string // resolver configuration lines
164	servers []string // expected name servers
165}{
166	{
167		name:    "golang.org",
168		lines:   []string{"nameserver 8.8.8.8"},
169		servers: []string{"8.8.8.8"},
170	},
171	{
172		name:    "",
173		lines:   nil, // an empty resolv.conf should use defaultNS as name servers
174		servers: defaultNS,
175	},
176	{
177		name:    "www.example.com",
178		lines:   []string{"nameserver 8.8.4.4"},
179		servers: []string{"8.8.4.4"},
180	},
181}
182
183func TestUpdateResolvConf(t *testing.T) {
184	if testing.Short() || !*testExternal {
185		t.Skip("avoid external network")
186	}
187
188	conf, err := newResolvConfTest()
189	if err != nil {
190		t.Fatal(err)
191	}
192	defer conf.teardown()
193
194	for i, tt := range updateResolvConfTests {
195		if err := conf.writeAndUpdate(tt.lines); err != nil {
196			t.Error(err)
197			continue
198		}
199		if tt.name != "" {
200			var wg sync.WaitGroup
201			const N = 10
202			wg.Add(N)
203			for j := 0; j < N; j++ {
204				go func(name string) {
205					defer wg.Done()
206					ips, err := goLookupIP(name)
207					if err != nil {
208						t.Error(err)
209						return
210					}
211					if len(ips) == 0 {
212						t.Errorf("no records for %s", name)
213						return
214					}
215				}(tt.name)
216			}
217			wg.Wait()
218		}
219		servers := conf.servers()
220		if !reflect.DeepEqual(servers, tt.servers) {
221			t.Errorf("#%d: got %v; want %v", i, servers, tt.servers)
222			continue
223		}
224	}
225}
226
227var goLookupIPWithResolverConfigTests = []struct {
228	name  string
229	lines []string // resolver configuration lines
230	error
231	a, aaaa bool // whether response contains A, AAAA-record
232}{
233	// no records, transport timeout
234	{
235		"jgahvsekduiv9bw4b3qhn4ykdfgj0493iohkrjfhdvhjiu4j",
236		[]string{
237			"options timeout:1 attempts:1",
238			"nameserver 255.255.255.255", // please forgive us for abuse of limited broadcast address
239		},
240		&DNSError{Name: "jgahvsekduiv9bw4b3qhn4ykdfgj0493iohkrjfhdvhjiu4j", Server: "255.255.255.255:53", IsTimeout: true},
241		false, false,
242	},
243
244	// no records, non-existent domain
245	{
246		"jgahvsekduiv9bw4b3qhn4ykdfgj0493iohkrjfhdvhjiu4j",
247		[]string{
248			"options timeout:3 attempts:1",
249			"nameserver 8.8.8.8",
250		},
251		&DNSError{Name: "jgahvsekduiv9bw4b3qhn4ykdfgj0493iohkrjfhdvhjiu4j", Server: "8.8.8.8:53", IsTimeout: false},
252		false, false,
253	},
254
255	// a few A records, no AAAA records
256	{
257		"ipv4.google.com.",
258		[]string{
259			"nameserver 8.8.8.8",
260			"nameserver 2001:4860:4860::8888",
261		},
262		nil,
263		true, false,
264	},
265	{
266		"ipv4.google.com",
267		[]string{
268			"domain golang.org",
269			"nameserver 2001:4860:4860::8888",
270			"nameserver 8.8.8.8",
271		},
272		nil,
273		true, false,
274	},
275	{
276		"ipv4.google.com",
277		[]string{
278			"search x.golang.org y.golang.org",
279			"nameserver 2001:4860:4860::8888",
280			"nameserver 8.8.8.8",
281		},
282		nil,
283		true, false,
284	},
285
286	// no A records, a few AAAA records
287	{
288		"ipv6.google.com.",
289		[]string{
290			"nameserver 2001:4860:4860::8888",
291			"nameserver 8.8.8.8",
292		},
293		nil,
294		false, true,
295	},
296	{
297		"ipv6.google.com",
298		[]string{
299			"domain golang.org",
300			"nameserver 8.8.8.8",
301			"nameserver 2001:4860:4860::8888",
302		},
303		nil,
304		false, true,
305	},
306	{
307		"ipv6.google.com",
308		[]string{
309			"search x.golang.org y.golang.org",
310			"nameserver 8.8.8.8",
311			"nameserver 2001:4860:4860::8888",
312		},
313		nil,
314		false, true,
315	},
316
317	// both A and AAAA records
318	{
319		"hostname.as112.net", // see RFC 7534
320		[]string{
321			"domain golang.org",
322			"nameserver 2001:4860:4860::8888",
323			"nameserver 8.8.8.8",
324		},
325		nil,
326		true, true,
327	},
328	{
329		"hostname.as112.net", // see RFC 7534
330		[]string{
331			"search x.golang.org y.golang.org",
332			"nameserver 2001:4860:4860::8888",
333			"nameserver 8.8.8.8",
334		},
335		nil,
336		true, true,
337	},
338}
339
340func TestGoLookupIPWithResolverConfig(t *testing.T) {
341	if testing.Short() || !*testExternal {
342		t.Skip("avoid external network")
343	}
344
345	conf, err := newResolvConfTest()
346	if err != nil {
347		t.Fatal(err)
348	}
349	defer conf.teardown()
350
351	for _, tt := range goLookupIPWithResolverConfigTests {
352		if err := conf.writeAndUpdate(tt.lines); err != nil {
353			t.Error(err)
354			continue
355		}
356		conf.tryUpdate(conf.path)
357		addrs, err := goLookupIP(tt.name)
358		if err != nil {
359			if err, ok := err.(*DNSError); !ok || (err.Name != tt.error.(*DNSError).Name || err.Server != tt.error.(*DNSError).Server || err.IsTimeout != tt.error.(*DNSError).IsTimeout) {
360				t.Errorf("got %v; want %v", err, tt.error)
361			}
362			continue
363		}
364		if len(addrs) == 0 {
365			t.Errorf("no records for %s", tt.name)
366		}
367		if !tt.a && !tt.aaaa && len(addrs) > 0 {
368			t.Errorf("unexpected %v for %s", addrs, tt.name)
369		}
370		for _, addr := range addrs {
371			if !tt.a && addr.IP.To4() != nil {
372				t.Errorf("got %v; must not be IPv4 address", addr)
373			}
374			if !tt.aaaa && addr.IP.To16() != nil && addr.IP.To4() == nil {
375				t.Errorf("got %v; must not be IPv6 address", addr)
376			}
377		}
378	}
379}
380
381// Test that goLookupIPOrder falls back to the host file when no DNS servers are available.
382func TestGoLookupIPOrderFallbackToFile(t *testing.T) {
383	if testing.Short() || !*testExternal {
384		t.Skip("avoid external network")
385	}
386
387	// Add a config that simulates no dns servers being available.
388	conf, err := newResolvConfTest()
389	if err != nil {
390		t.Fatal(err)
391	}
392	if err := conf.writeAndUpdate([]string{}); err != nil {
393		t.Fatal(err)
394	}
395	conf.tryUpdate(conf.path)
396	// Redirect host file lookups.
397	defer func(orig string) { testHookHostsPath = orig }(testHookHostsPath)
398	testHookHostsPath = "testdata/hosts"
399
400	for _, order := range []hostLookupOrder{hostLookupFilesDNS, hostLookupDNSFiles} {
401		name := fmt.Sprintf("order %v", order)
402
403		// First ensure that we get an error when contacting a non-existant host.
404		_, err := goLookupIPOrder("notarealhost", order)
405		if err == nil {
406			t.Errorf("%s: expected error while looking up name not in hosts file", name)
407			continue
408		}
409
410		// Now check that we get an address when the name appears in the hosts file.
411		addrs, err := goLookupIPOrder("thor", order) // entry is in "testdata/hosts"
412		if err != nil {
413			t.Errorf("%s: expected to successfully lookup host entry", name)
414			continue
415		}
416		if len(addrs) != 1 {
417			t.Errorf("%s: expected exactly one result, but got %v", name, addrs)
418			continue
419		}
420		if got, want := addrs[0].String(), "127.1.1.1"; got != want {
421			t.Errorf("%s: address doesn't match expectation. got %v, want %v", name, got, want)
422		}
423	}
424	defer conf.teardown()
425}
426
427// Issue 12712.
428// When using search domains, return the error encountered
429// querying the original name instead of an error encountered
430// querying a generated name.
431func TestErrorForOriginalNameWhenSearching(t *testing.T) {
432	const fqdn = "doesnotexist.domain"
433
434	origTestHookDNSDialer := testHookDNSDialer
435	defer func() { testHookDNSDialer = origTestHookDNSDialer }()
436
437	conf, err := newResolvConfTest()
438	if err != nil {
439		t.Fatal(err)
440	}
441	defer conf.teardown()
442
443	if err := conf.writeAndUpdate([]string{"search servfail"}); err != nil {
444		t.Fatal(err)
445	}
446
447	d := &fakeDNSConn{}
448	testHookDNSDialer = func(time.Duration) dnsDialer { return d }
449
450	d.rh = func(q *dnsMsg) (*dnsMsg, error) {
451		r := &dnsMsg{
452			dnsMsgHdr: dnsMsgHdr{
453				id: q.id,
454			},
455		}
456
457		switch q.question[0].Name {
458		case fqdn + ".servfail.":
459			r.rcode = dnsRcodeServerFailure
460		default:
461			r.rcode = dnsRcodeNameError
462		}
463
464		return r, nil
465	}
466
467	_, err = goLookupIP(fqdn)
468	if err == nil {
469		t.Fatal("expected an error")
470	}
471
472	want := &DNSError{Name: fqdn, Err: errNoSuchHost.Error()}
473	if err, ok := err.(*DNSError); !ok || err.Name != want.Name || err.Err != want.Err {
474		t.Errorf("got %v; want %v", err, want)
475	}
476}
477
478func BenchmarkGoLookupIP(b *testing.B) {
479	testHookUninstaller.Do(uninstallTestHooks)
480
481	for i := 0; i < b.N; i++ {
482		goLookupIP("www.example.com")
483	}
484}
485
486func BenchmarkGoLookupIPNoSuchHost(b *testing.B) {
487	testHookUninstaller.Do(uninstallTestHooks)
488
489	for i := 0; i < b.N; i++ {
490		goLookupIP("some.nonexistent")
491	}
492}
493
494func BenchmarkGoLookupIPWithBrokenNameServer(b *testing.B) {
495	testHookUninstaller.Do(uninstallTestHooks)
496
497	conf, err := newResolvConfTest()
498	if err != nil {
499		b.Fatal(err)
500	}
501	defer conf.teardown()
502
503	lines := []string{
504		"nameserver 203.0.113.254", // use TEST-NET-3 block, see RFC 5737
505		"nameserver 8.8.8.8",
506	}
507	if err := conf.writeAndUpdate(lines); err != nil {
508		b.Fatal(err)
509	}
510
511	for i := 0; i < b.N; i++ {
512		goLookupIP("www.example.com")
513	}
514}
515
516type fakeDNSConn struct {
517	// last query
518	qmu sync.Mutex // guards q
519	q   *dnsMsg
520	// reply handler
521	rh func(*dnsMsg) (*dnsMsg, error)
522}
523
524func (f *fakeDNSConn) dialDNS(n, s string) (dnsConn, error) {
525	return f, nil
526}
527
528func (f *fakeDNSConn) Close() error {
529	return nil
530}
531
532func (f *fakeDNSConn) SetDeadline(time.Time) error {
533	return nil
534}
535
536func (f *fakeDNSConn) writeDNSQuery(q *dnsMsg) error {
537	f.qmu.Lock()
538	defer f.qmu.Unlock()
539	f.q = q
540	return nil
541}
542
543func (f *fakeDNSConn) readDNSResponse() (*dnsMsg, error) {
544	f.qmu.Lock()
545	q := f.q
546	f.qmu.Unlock()
547	return f.rh(q)
548}
549