"io/fs"
"strings"
"testing"
+ "time"
)
type nssHostTest struct {
tests := []struct {
name string
c *conf
+ nss *nssConf
resolver *Resolver
hostTests []nssHostTest
}{
name: "force",
c: &conf{
forceCgoLookupHost: true,
- nss: nssStr("foo: bar"),
resolv: defaultResolvConf,
},
+ nss: nssStr("foo: bar"),
hostTests: []nssHostTest{
{"foo.local", "myhostname", hostLookupCgo},
{"google.com", "myhostname", hostLookupCgo},
name: "netgo_dns_before_files",
c: &conf{
netGo: true,
- nss: nssStr("hosts: dns files"),
resolv: defaultResolvConf,
},
+ nss: nssStr("hosts: dns files"),
hostTests: []nssHostTest{
{"x.com", "myhostname", hostLookupDNSFiles},
},
name: "netgo_fallback_on_cgo",
c: &conf{
netGo: true,
- nss: nssStr("hosts: dns files something_custom"),
resolv: defaultResolvConf,
},
+ nss: nssStr("hosts: dns files something_custom"),
hostTests: []nssHostTest{
{"x.com", "myhostname", hostLookupFilesDNS},
},
{
name: "ubuntu_trusty_avahi",
c: &conf{
- nss: nssStr("hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4"),
resolv: defaultResolvConf,
},
+ nss: nssStr("hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4"),
hostTests: []nssHostTest{
{"foo.local", "myhostname", hostLookupCgo},
{"foo.local.", "myhostname", hostLookupCgo},
name: "freebsdlinux_no_resolv_conf",
c: &conf{
goos: "freebsd",
- nss: nssStr("foo: bar"),
resolv: defaultResolvConf,
},
+ nss: nssStr("foo: bar"),
hostTests: []nssHostTest{{"google.com", "myhostname", hostLookupFilesDNS}},
},
// On OpenBSD, no resolv.conf means no DNS.
name: "solaris_no_nsswitch",
c: &conf{
goos: "solaris",
- nss: &nssConf{err: fs.ErrNotExist},
resolv: defaultResolvConf,
},
+ nss: &nssConf{err: fs.ErrNotExist},
hostTests: []nssHostTest{{"google.com", "myhostname", hostLookupCgo}},
},
{
name: "linux_no_nsswitch.conf",
c: &conf{
goos: "linux",
- nss: &nssConf{err: fs.ErrNotExist},
resolv: defaultResolvConf,
},
+ nss: &nssConf{err: fs.ErrNotExist},
hostTests: []nssHostTest{{"google.com", "myhostname", hostLookupFilesDNS}},
},
{
name: "linux_empty_nsswitch.conf",
c: &conf{
goos: "linux",
- nss: nssStr(""),
resolv: defaultResolvConf,
},
+ nss: nssStr(""),
hostTests: []nssHostTest{{"google.com", "myhostname", hostLookupFilesDNS}},
},
{
name: "files_mdns_dns",
c: &conf{
- nss: nssStr("hosts: files mdns dns"),
resolv: defaultResolvConf,
},
+ nss: nssStr("hosts: files mdns dns"),
hostTests: []nssHostTest{
{"x.com", "myhostname", hostLookupFilesDNS},
{"x.local", "myhostname", hostLookupCgo},
{
name: "dns_special_hostnames",
c: &conf{
- nss: nssStr("hosts: dns"),
resolv: defaultResolvConf,
},
+ nss: nssStr("hosts: dns"),
hostTests: []nssHostTest{
{"x.com", "myhostname", hostLookupDNS},
{"x\\.com", "myhostname", hostLookupCgo}, // punt on weird glibc escape
{
name: "mdns_allow",
c: &conf{
- nss: nssStr("hosts: files mdns dns"),
resolv: defaultResolvConf,
hasMDNSAllow: true,
},
+ nss: nssStr("hosts: files mdns dns"),
hostTests: []nssHostTest{
{"x.com", "myhostname", hostLookupCgo},
{"x.local", "myhostname", hostLookupCgo},
{
name: "files_dns",
c: &conf{
- nss: nssStr("hosts: files dns"),
resolv: defaultResolvConf,
},
+ nss: nssStr("hosts: files dns"),
hostTests: []nssHostTest{
{"x.com", "myhostname", hostLookupFilesDNS},
{"x", "myhostname", hostLookupFilesDNS},
{
name: "dns_files",
c: &conf{
- nss: nssStr("hosts: dns files"),
resolv: defaultResolvConf,
},
+ nss: nssStr("hosts: dns files"),
hostTests: []nssHostTest{
{"x.com", "myhostname", hostLookupDNSFiles},
{"x", "myhostname", hostLookupDNSFiles},
{
name: "something_custom",
c: &conf{
- nss: nssStr("hosts: dns files something_custom"),
resolv: defaultResolvConf,
},
+ nss: nssStr("hosts: dns files something_custom"),
hostTests: []nssHostTest{
{"x.com", "myhostname", hostLookupCgo},
},
{
name: "myhostname",
c: &conf{
- nss: nssStr("hosts: files dns myhostname"),
resolv: defaultResolvConf,
},
+ nss: nssStr("hosts: files dns myhostname"),
hostTests: []nssHostTest{
{"x.com", "myhostname", hostLookupFilesDNS},
{"myhostname", "myhostname", hostLookupCgo},
{
name: "ubuntu14.04.02",
c: &conf{
- nss: nssStr("hosts: files myhostname mdns4_minimal [NOTFOUND=return] dns mdns4"),
resolv: defaultResolvConf,
},
+ nss: nssStr("hosts: files myhostname mdns4_minimal [NOTFOUND=return] dns mdns4"),
hostTests: []nssHostTest{
{"x.com", "myhostname", hostLookupFilesDNS},
{"somehostname", "myhostname", hostLookupFilesDNS},
{
name: "debian_squeeze",
c: &conf{
- nss: nssStr("hosts: dns [success=return notfound=continue unavail=continue tryagain=continue] files [notfound=return]"),
resolv: defaultResolvConf,
},
+ nss: nssStr("hosts: dns [success=return notfound=continue unavail=continue tryagain=continue] files [notfound=return]"),
hostTests: []nssHostTest{
{"x.com", "myhostname", hostLookupDNSFiles},
{"somehostname", "myhostname", hostLookupDNSFiles},
{
name: "resolv.conf-unknown",
c: &conf{
- nss: nssStr("foo: bar"),
resolv: &dnsConfig{servers: defaultNS, ndots: 1, timeout: 5, attempts: 2, unknownOpt: true},
},
+ nss: nssStr("foo: bar"),
hostTests: []nssHostTest{{"google.com", "myhostname", hostLookupCgo}},
},
// Android should always use cgo.
name: "android",
c: &conf{
goos: "android",
- nss: nssStr(""),
resolv: defaultResolvConf,
},
+ nss: nssStr(""),
hostTests: []nssHostTest{
{"x.com", "myhostname", hostLookupCgo},
},
goos: "darwin",
forceCgoLookupHost: true, // always true for darwin
resolv: defaultResolvConf,
- nss: nssStr(""),
netCgo: true,
},
+ nss: nssStr(""),
hostTests: []nssHostTest{
{"localhost", "myhostname", hostLookupFilesDNS},
},
origGetHostname := getHostname
defer func() { getHostname = origGetHostname }()
+ defer setSystemNSS(getSystemNSS(), 0)
for _, tt := range tests {
+
for _, ht := range tt.hostTests {
getHostname = func() (string, error) { return ht.localhost, nil }
+ setSystemNSS(tt.nss, time.Hour)
gotOrder := tt.c.hostLookupOrder(tt.resolver, ht.host)
if gotOrder != ht.want {
}
}
}
+}
+func setSystemNSS(nss *nssConf, addDur time.Duration) {
+ nssConfig.mu.Lock()
+ nssConfig.nssConf = nss
+ nssConfig.mu.Unlock()
+ nssConfig.acquireSema()
+ nssConfig.lastChecked = time.Now().Add(addDur)
+ nssConfig.releaseSema()
}
func TestSystemConf(t *testing.T) {
"internal/bytealg"
"io"
"os"
+ "sync"
+ "time"
)
+const (
+ nssConfigPath = "/etc/nsswitch.conf"
+)
+
+var nssConfig nsswitchConfig
+
+type nsswitchConfig struct {
+ initOnce sync.Once // guards init of nsswitchConfig
+
+ // ch is used as a semaphore that only allows one lookup at a
+ // time to recheck nsswitch.conf
+ ch chan struct{} // guards lastChecked and modTime
+ lastChecked time.Time // last time nsswitch.conf was checked
+
+ mu sync.Mutex // protects nssConf
+ nssConf *nssConf
+}
+
+func getSystemNSS() *nssConf {
+ nssConfig.tryUpdate()
+ nssConfig.mu.Lock()
+ conf := nssConfig.nssConf
+ nssConfig.mu.Unlock()
+ return conf
+}
+
+// init initializes conf and is only called via conf.initOnce.
+func (conf *nsswitchConfig) init() {
+ conf.nssConf = parseNSSConfFile("/etc/nsswitch.conf")
+ conf.lastChecked = time.Now()
+ conf.ch = make(chan struct{}, 1)
+}
+
+// tryUpdate tries to update conf.
+func (conf *nsswitchConfig) tryUpdate() {
+ conf.initOnce.Do(conf.init)
+
+ // Ensure only one update at a time checks nsswitch.conf
+ if !conf.tryAcquireSema() {
+ return
+ }
+ defer conf.releaseSema()
+
+ now := time.Now()
+ if conf.lastChecked.After(now.Add(-5 * time.Second)) {
+ return
+ }
+ conf.lastChecked = now
+
+ var mtime time.Time
+ if fi, err := os.Stat(nssConfigPath); err == nil {
+ mtime = fi.ModTime()
+ }
+ if mtime.Equal(conf.nssConf.mtime) {
+ return
+ }
+
+ nssConf := parseNSSConfFile(nssConfigPath)
+ conf.mu.Lock()
+ conf.nssConf = nssConf
+ conf.mu.Unlock()
+}
+
+func (conf *nsswitchConfig) acquireSema() {
+ conf.ch <- struct{}{}
+}
+
+func (conf *nsswitchConfig) tryAcquireSema() bool {
+ select {
+ case conf.ch <- struct{}{}:
+ return true
+ default:
+ return false
+ }
+}
+
+func (conf *nsswitchConfig) releaseSema() {
+ <-conf.ch
+}
+
// nssConf represents the state of the machine's /etc/nsswitch.conf file.
type nssConf struct {
+ mtime time.Time // time of nsswitch.conf modification
err error // any error encountered opening or parsing the file
sources map[string][]nssSource // keyed by database (e.g. "hosts")
}
return &nssConf{err: err}
}
defer f.Close()
- return parseNSSConf(f)
+ stat, err := f.Stat()
+ if err != nil {
+ return &nssConf{err: err}
+ }
+
+ conf := parseNSSConf(f)
+ conf.mtime = stat.ModTime()
+ return conf
}
func parseNSSConf(r io.Reader) *nssConf {