Commit 7ef6a9f3 authored by Mikio Hara's avatar Mikio Hara Committed by Brad Fitzpatrick

net: clean up builtin DNS stub resolver, fix tests

This change does clean up as preparation for fixing #11081.

- renames cfg to resolvConf for clarification
- adds a new type resolverConfig and its methods: init, update,
  tryAcquireSema, releaseSema for mutual exclusion of resolv.conf data
- deflakes, simplifies tests for resolv.conf data; previously the tests
  sometimes left some garbage in the data

Change-Id: I277ced853fddc3791dde40ab54dbd5c78114b78c
Reviewed-on: https://go-review.googlesource.com/10931Reviewed-by: default avatarBrad Fitzpatrick <bradfitz@golang.org>
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
parent d7a8d3eb
...@@ -195,30 +195,30 @@ func tryOneName(cfg *dnsConfig, name string, qtype uint16) (string, []dnsRR, err ...@@ -195,30 +195,30 @@ func tryOneName(cfg *dnsConfig, name string, qtype uint16) (string, []dnsRR, err
return "", nil, lastErr return "", nil, lastErr
} }
func convertRR_A(records []dnsRR) []IP { // addrRecordList converts and returns a list of IP addresses from DNS
addrs := make([]IP, len(records)) // address records (both A and AAAA). Other record types are ignored.
for i, rr := range records { func addrRecordList(rrs []dnsRR) []IPAddr {
a := rr.(*dnsRR_A).A addrs := make([]IPAddr, 0, 4)
addrs[i] = IPv4(byte(a>>24), byte(a>>16), byte(a>>8), byte(a)) for _, rr := range rrs {
switch rr := rr.(type) {
case *dnsRR_A:
addrs = append(addrs, IPAddr{IP: IPv4(byte(rr.A>>24), byte(rr.A>>16), byte(rr.A>>8), byte(rr.A))})
case *dnsRR_AAAA:
ip := make(IP, IPv6len)
copy(ip, rr.AAAA[:])
addrs = append(addrs, IPAddr{IP: ip})
} }
return addrs
}
func convertRR_AAAA(records []dnsRR) []IP {
addrs := make([]IP, len(records))
for i, rr := range records {
a := make(IP, IPv6len)
copy(a, rr.(*dnsRR_AAAA).AAAA[:])
addrs[i] = a
} }
return addrs return addrs
} }
// cfg is used for the storage and reparsing of /etc/resolv.conf // A resolverConfig represents a DNS stub resolver configuration.
var cfg struct { type resolverConfig struct {
// ch is used as a semaphore that only allows one lookup at a time to initOnce sync.Once // guards init of resolverConfig
// recheck resolv.conf. It acts as guard for lastChecked and modTime.
ch chan struct{} // ch is used as a semaphore that only allows one lookup at a
// time to recheck resolv.conf.
ch chan struct{} // guards lastChecked and modTime
lastChecked time.Time // last time resolv.conf was checked lastChecked time.Time // last time resolv.conf was checked
modTime time.Time // time of resolv.conf modification modTime time.Time // time of resolv.conf modification
...@@ -226,60 +226,75 @@ var cfg struct { ...@@ -226,60 +226,75 @@ var cfg struct {
dnsConfig *dnsConfig // parsed resolv.conf structure used in lookups dnsConfig *dnsConfig // parsed resolv.conf structure used in lookups
} }
var onceLoadConfig sync.Once var resolvConf resolverConfig
func initCfg() { // init initializes conf and is only called via conf.initOnce.
func (conf *resolverConfig) init() {
// Set dnsConfig, modTime, and lastChecked so we don't parse // Set dnsConfig, modTime, and lastChecked so we don't parse
// resolv.conf twice the first time. // resolv.conf twice the first time.
cfg.dnsConfig = systemConf().resolv conf.dnsConfig = systemConf().resolv
if cfg.dnsConfig == nil { if conf.dnsConfig == nil {
cfg.dnsConfig = dnsReadConfig("/etc/resolv.conf") conf.dnsConfig = dnsReadConfig("/etc/resolv.conf")
} }
if fi, err := os.Stat("/etc/resolv.conf"); err == nil { if fi, err := os.Stat("/etc/resolv.conf"); err == nil {
cfg.modTime = fi.ModTime() conf.modTime = fi.ModTime()
} }
cfg.lastChecked = time.Now() conf.lastChecked = time.Now()
// Prepare ch so that only one loadConfig may run at once // Prepare ch so that only one update of resolverConfig may
cfg.ch = make(chan struct{}, 1) // run at once.
cfg.ch <- struct{}{} conf.ch = make(chan struct{}, 1)
} }
func loadConfig(resolvConfPath string) { // tryUpdate tries to update conf with the named resolv.conf file.
onceLoadConfig.Do(initCfg) // The name variable only exists for testing. It is otherwise always
// "/etc/resolv.conf".
func (conf *resolverConfig) tryUpdate(name string) {
conf.initOnce.Do(conf.init)
// ensure only one loadConfig at a time checks /etc/resolv.conf // Ensure only one update at a time checks resolv.conf.
select { if !conf.tryAcquireSema() {
case <-cfg.ch:
defer func() { cfg.ch <- struct{}{} }()
default:
return return
} }
defer conf.releaseSema()
now := time.Now() now := time.Now()
if cfg.lastChecked.After(now.Add(-5 * time.Second)) { if conf.lastChecked.After(now.Add(-5 * time.Second)) {
return return
} }
cfg.lastChecked = now conf.lastChecked = now
if fi, err := os.Stat(resolvConfPath); err == nil { if fi, err := os.Stat(name); err == nil {
if fi.ModTime().Equal(cfg.modTime) { if fi.ModTime().Equal(conf.modTime) {
return return
} }
cfg.modTime = fi.ModTime() conf.modTime = fi.ModTime()
} else { } else {
// If modTime wasn't set prior, assume nothing has changed. // If modTime wasn't set prior, assume nothing has changed.
if cfg.modTime.IsZero() { if conf.modTime.IsZero() {
return return
} }
cfg.modTime = time.Time{} conf.modTime = time.Time{}
}
dnsConf := dnsReadConfig(name)
conf.mu.Lock()
conf.dnsConfig = dnsConf
conf.mu.Unlock()
}
func (conf *resolverConfig) tryAcquireSema() bool {
select {
case conf.ch <- struct{}{}:
return true
default:
return false
} }
}
ncfg := dnsReadConfig(resolvConfPath) func (conf *resolverConfig) releaseSema() {
cfg.mu.Lock() <-conf.ch
cfg.dnsConfig = ncfg
cfg.mu.Unlock()
} }
func lookup(name string, qtype uint16) (cname string, rrs []dnsRR, err error) { func lookup(name string, qtype uint16) (cname string, rrs []dnsRR, err error) {
...@@ -287,10 +302,10 @@ func lookup(name string, qtype uint16) (cname string, rrs []dnsRR, err error) { ...@@ -287,10 +302,10 @@ func lookup(name string, qtype uint16) (cname string, rrs []dnsRR, err error) {
return name, nil, &DNSError{Err: "invalid domain name", Name: name} return name, nil, &DNSError{Err: "invalid domain name", Name: name}
} }
loadConfig("/etc/resolv.conf") resolvConf.tryUpdate("/etc/resolv.conf")
cfg.mu.RLock() resolvConf.mu.RLock()
resolv := cfg.dnsConfig resolv := resolvConf.dnsConfig
cfg.mu.RUnlock() resolvConf.mu.RUnlock()
// If name is rooted (trailing dot) or has enough dots, // If name is rooted (trailing dot) or has enough dots,
// try it by itself first. // try it by itself first.
...@@ -441,18 +456,7 @@ func goLookupIPOrder(name string, order hostLookupOrder) (addrs []IPAddr, err er ...@@ -441,18 +456,7 @@ func goLookupIPOrder(name string, order hostLookupOrder) (addrs []IPAddr, err er
lastErr = racer.error lastErr = racer.error
continue continue
} }
switch racer.qtype { addrs = append(addrs, addrRecordList(racer.rrs)...)
case dnsTypeA:
for _, ip := range convertRR_A(racer.rrs) {
addr := IPAddr{IP: ip}
addrs = append(addrs, addr)
}
case dnsTypeAAAA:
for _, ip := range convertRR_AAAA(racer.rrs) {
addr := IPAddr{IP: ip}
addrs = append(addrs, addr)
}
}
} }
if len(addrs) == 0 { if len(addrs) == 0 {
if lastErr != nil { if lastErr != nil {
...@@ -472,11 +476,11 @@ func goLookupIPOrder(name string, order hostLookupOrder) (addrs []IPAddr, err er ...@@ -472,11 +476,11 @@ func goLookupIPOrder(name string, order hostLookupOrder) (addrs []IPAddr, err er
// depending on our lookup code, so that Go and C get the same // depending on our lookup code, so that Go and C get the same
// answers. // answers.
func goLookupCNAME(name string) (cname string, err error) { func goLookupCNAME(name string) (cname string, err error) {
_, rr, err := lookup(name, dnsTypeCNAME) _, rrs, err := lookup(name, dnsTypeCNAME)
if err != nil { if err != nil {
return return
} }
cname = rr[0].(*dnsRR_CNAME).Cname cname = rrs[0].(*dnsRR_CNAME).Cname
return return
} }
......
...@@ -7,11 +7,13 @@ ...@@ -7,11 +7,13 @@
package net package net
import ( import (
"io" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"path" "path"
"reflect" "reflect"
"strings"
"sync"
"testing" "testing"
"time" "time"
) )
...@@ -93,117 +95,133 @@ func TestSpecialDomainName(t *testing.T) { ...@@ -93,117 +95,133 @@ func TestSpecialDomainName(t *testing.T) {
} }
type resolvConfTest struct { type resolvConfTest struct {
*testing.T
dir string dir string
path string path string
*resolverConfig
} }
func newResolvConfTest(t *testing.T) *resolvConfTest { func newResolvConfTest() (*resolvConfTest, error) {
dir, err := ioutil.TempDir("", "go-resolvconftest") dir, err := ioutil.TempDir("", "go-resolvconftest")
if err != nil { if err != nil {
t.Fatal(err) return nil, err
} }
conf := &resolvConfTest{
r := &resolvConfTest{
T: t,
dir: dir, dir: dir,
path: path.Join(dir, "resolv.conf"), path: path.Join(dir, "resolv.conf"),
resolverConfig: &resolvConf,
} }
conf.initOnce.Do(conf.init)
return r return conf, nil
} }
func (r *resolvConfTest) SetConf(s string) { func (conf *resolvConfTest) writeAndUpdate(lines []string) error {
// Make sure the file mtime will be different once we're done here, f, err := os.OpenFile(conf.path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
// even on systems with coarse (1s) mtime resolution.
time.Sleep(time.Second)
f, err := os.OpenFile(r.path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
if err != nil { if err != nil {
r.Fatalf("failed to create temp file %s: %v", r.path, err) return err
} }
if _, err := io.WriteString(f, s); err != nil { if _, err := f.WriteString(strings.Join(lines, "\n")); err != nil {
f.Close() f.Close()
r.Fatalf("failed to write temp file: %v", err) return err
} }
f.Close() f.Close()
cfg.lastChecked = time.Time{} if err := conf.forceUpdate(conf.path); err != nil {
loadConfig(r.path) return err
}
return nil
} }
func (r *resolvConfTest) WantServers(want []string) { func (conf *resolvConfTest) forceUpdate(name string) error {
cfg.mu.RLock() dnsConf := dnsReadConfig(name)
defer cfg.mu.RUnlock() conf.mu.Lock()
if got := cfg.dnsConfig.servers; !reflect.DeepEqual(got, want) { conf.dnsConfig = dnsConf
r.Fatalf("unexpected dns server loaded, got %v want %v", got, want) conf.mu.Unlock()
for i := 0; i < 5; i++ {
if conf.tryAcquireSema() {
conf.lastChecked = time.Time{}
conf.releaseSema()
return nil
}
} }
return fmt.Errorf("tryAcquireSema for %s failed", name)
} }
func (r *resolvConfTest) Close() { func (conf *resolvConfTest) servers() []string {
if err := os.RemoveAll(r.dir); err != nil { conf.mu.RLock()
r.Logf("failed to remove temp dir %s: %v", r.dir, err) servers := conf.dnsConfig.servers
} conf.mu.RUnlock()
return servers
}
func (conf *resolvConfTest) teardown() error {
err := conf.forceUpdate("/etc/resolv.conf")
os.RemoveAll(conf.dir)
return err
} }
func TestReloadResolvConfFail(t *testing.T) { var updateResolvConfTests = []struct {
name string // query name
lines []string // resolver configuration lines
servers []string // expected name servers
}{
{
name: "golang.org",
lines: []string{"nameserver 8.8.8.8"},
servers: []string{"8.8.8.8"},
},
{
name: "",
lines: nil, // an empty resolv.conf should use defaultNS as name servers
servers: defaultNS,
},
{
name: "www.example.com",
lines: []string{"nameserver 8.8.4.4"},
servers: []string{"8.8.4.4"},
},
}
func TestUpdateResolvConf(t *testing.T) {
if testing.Short() || !*testExternal { if testing.Short() || !*testExternal {
t.Skip("avoid external network") t.Skip("avoid external network")
} }
r := newResolvConfTest(t) conf, err := newResolvConfTest()
defer r.Close() if err != nil {
r.SetConf("nameserver 8.8.8.8")
if _, err := goLookupIP("golang.org"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer conf.teardown()
// Using an empty resolv.conf should use localhost as servers for i, tt := range updateResolvConfTests {
r.SetConf("") if err := conf.writeAndUpdate(tt.lines); err != nil {
t.Error(err)
if len(cfg.dnsConfig.servers) != len(defaultNS) { continue
t.Fatalf("goLookupIP(missing; good; bad) failed: servers=%v, want: %v", cfg.dnsConfig.servers, defaultNS)
}
for i := range cfg.dnsConfig.servers {
if cfg.dnsConfig.servers[i] != defaultNS[i] {
t.Fatalf("goLookupIP(missing; good; bad) failed: servers=%v, want: %v", cfg.dnsConfig.servers, defaultNS)
} }
if tt.name != "" {
var wg sync.WaitGroup
const N = 10
wg.Add(N)
for j := 0; j < N; j++ {
go func(name string) {
defer wg.Done()
ips, err := goLookupIP(name)
if err != nil {
t.Error(err)
return
} }
} if len(ips) == 0 {
t.Errorf("no records for %s", name)
func TestReloadResolvConfChange(t *testing.T) { return
if testing.Short() || !*testExternal {
t.Skip("avoid external network")
} }
}(tt.name)
r := newResolvConfTest(t)
defer r.Close()
r.SetConf("nameserver 8.8.8.8")
if _, err := goLookupIP("golang.org"); err != nil {
t.Fatal(err)
} }
r.WantServers([]string{"8.8.8.8"}) wg.Wait()
// Using an empty resolv.conf should use localhost as servers
r.SetConf("")
if len(cfg.dnsConfig.servers) != len(defaultNS) {
t.Fatalf("goLookupIP(missing; good; bad) failed: servers=%v, want: %v", cfg.dnsConfig.servers, defaultNS)
} }
servers := conf.servers()
for i := range cfg.dnsConfig.servers { if !reflect.DeepEqual(servers, tt.servers) {
if cfg.dnsConfig.servers[i] != defaultNS[i] { t.Errorf("#%d: got %v; want %v", i, servers, tt.servers)
t.Fatalf("goLookupIP(missing; good; bad) failed: servers=%v, want: %v", cfg.dnsConfig.servers, defaultNS) continue
} }
} }
// A new good config should get picked up
r.SetConf("nameserver 8.8.4.4")
r.WantServers([]string{"8.8.4.4"})
} }
func BenchmarkGoLookupIP(b *testing.B) { func BenchmarkGoLookupIP(b *testing.B) {
...@@ -225,14 +243,21 @@ func BenchmarkGoLookupIPNoSuchHost(b *testing.B) { ...@@ -225,14 +243,21 @@ func BenchmarkGoLookupIPNoSuchHost(b *testing.B) {
func BenchmarkGoLookupIPWithBrokenNameServer(b *testing.B) { func BenchmarkGoLookupIPWithBrokenNameServer(b *testing.B) {
testHookUninstaller.Do(uninstallTestHooks) testHookUninstaller.Do(uninstallTestHooks)
// This looks ugly but it's safe as long as benchmarks are run conf, err := newResolvConfTest()
// sequentially in package testing. if err != nil {
<-cfg.ch // keep config from being reloaded upon lookup b.Fatal(err)
orig := cfg.dnsConfig }
cfg.dnsConfig.servers = append([]string{"203.0.113.254"}, cfg.dnsConfig.servers...) // use TEST-NET-3 block, see RFC 5737 defer conf.teardown()
lines := []string{
"nameserver 203.0.113.254", // use TEST-NET-3 block, see RFC 5737
"nameserver 8.8.8.8",
}
if err := conf.writeAndUpdate(lines); err != nil {
b.Fatal(err)
}
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
goLookupIP("www.example.com") goLookupIP("www.example.com")
} }
cfg.dnsConfig = orig
cfg.ch <- struct{}{}
} }
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment