net: use Go's DNS resolver when system configuration permits

If the machine's network configuration files (resolv.conf,
nsswitch.conf) don't have any unsupported options, prefer Go's DNS
resolver, which doesn't have the cgo & thread over.

It means users can have more than 500 DNS requests outstanding (our
current limit for cgo lookups) and not have one blocked thread per
outstanding request.

Discussed in thread https://groups.google.com/d/msg/golang-dev/2ZUi792oztM/Q0rg_DkF5HMJ

Change-Id: I3f685d70aff6b47bec30b63e9fba674b20507f95
Reviewed-on: https://go-review.googlesource.com/8945
Reviewed-by: Ian Lance Taylor <iant@golang.org>
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/src/net/conf.go b/src/net/conf.go
new file mode 100644
index 0000000..9e8faca
--- /dev/null
+++ b/src/net/conf.go
@@ -0,0 +1,237 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build darwin dragonfly freebsd linux netbsd openbsd solaris
+
+package net
+
+import (
+	"os"
+	"runtime"
+	"sync"
+	"syscall"
+)
+
+// conf represents a system's network configuration.
+type conf struct {
+	// forceCgoLookupHost forces CGO to always be used, if available.
+	forceCgoLookupHost bool
+
+	// machine has an /etc/mdns.allow file
+	hasMDNSAllow bool
+
+	goos string // the runtime.GOOS, to ease testing
+
+	nss    *nssConf
+	resolv *dnsConfig
+}
+
+var (
+	confOnce sync.Once // guards init of confVal via initConfVal
+	confVal  = &conf{goos: runtime.GOOS}
+)
+
+// systemConf returns the machine's network configuration.
+func systemConf() *conf {
+	confOnce.Do(initConfVal)
+	return confVal
+}
+
+func initConfVal() {
+	// Darwin pops up annoying dialog boxes if programs try to do
+	// their own DNS requests. So always use cgo instead, which
+	// avoids that.
+	if runtime.GOOS == "darwin" {
+		confVal.forceCgoLookupHost = true
+		return
+	}
+
+	// If any environment-specified resolver options are specified,
+	// force cgo. Note that LOCALDOMAIN can change behavior merely
+	// by being specified with the empty string.
+	_, localDomainDefined := syscall.Getenv("LOCALDOMAIN")
+	if os.Getenv("RES_OPTIONS") != "" || os.Getenv("HOSTALIASES") != "" ||
+		localDomainDefined {
+		confVal.forceCgoLookupHost = true
+		return
+	}
+
+	// OpenBSD apparently lets you override the location of resolv.conf
+	// with ASR_CONFIG. If we notice that, defer to libc.
+	if runtime.GOOS == "openbsd" && os.Getenv("ASR_CONFIG") != "" {
+		confVal.forceCgoLookupHost = true
+		return
+	}
+
+	if runtime.GOOS != "openbsd" {
+		confVal.nss = parseNSSConfFile("/etc/nsswitch.conf")
+	}
+
+	if resolv, err := dnsReadConfig("/etc/resolv.conf"); err == nil {
+		confVal.resolv = resolv
+	} else if !os.IsNotExist(err.(*DNSConfigError).Err) {
+		// If we can't read the resolv.conf file, assume it
+		// had something important in it and defer to cgo.
+		// libc's resolver might then fail too, but at least
+		// it wasn't our fault.
+		confVal.forceCgoLookupHost = true
+	}
+
+	if _, err := os.Stat("/etc/mdns.allow"); err == nil {
+		confVal.hasMDNSAllow = true
+	}
+}
+
+// hostLookupOrder determines which strategy to use to resolve hostname.
+func (c *conf) hostLookupOrder(hostname string) hostLookupOrder {
+	if c.forceCgoLookupHost {
+		return hostLookupCgo
+	}
+	if byteIndex(hostname, '\\') != -1 || byteIndex(hostname, '%') != -1 {
+		// Don't deal with special form hostnames with backslashes
+		// or '%'.
+		return hostLookupCgo
+	}
+
+	// OpenBSD is unique and doesn't use nsswitch.conf.
+	// It also doesn't support mDNS.
+	if c.goos == "openbsd" {
+		// OpenBSD's resolv.conf manpage says that a non-existent
+		// resolv.conf means "lookup" defaults to only "files",
+		// without DNS lookups.
+		if c.resolv == nil {
+			return hostLookupFiles
+		}
+		lookup := c.resolv.lookup
+		if len(lookup) == 0 {
+			// http://www.openbsd.org/cgi-bin/man.cgi/OpenBSD-current/man5/resolv.conf.5
+			// "If the lookup keyword is not used in the
+			// system's resolv.conf file then the assumed
+			// order is 'bind file'"
+			return hostLookupDNSFiles
+		}
+		if len(lookup) < 1 || len(lookup) > 2 {
+			return hostLookupCgo
+		}
+		switch lookup[0] {
+		case "bind":
+			if len(lookup) == 2 {
+				if lookup[1] == "file" {
+					return hostLookupDNSFiles
+				}
+				return hostLookupCgo
+			}
+			return hostLookupDNS
+		case "file":
+			if len(lookup) == 2 {
+				if lookup[1] == "bind" {
+					return hostLookupFilesDNS
+				}
+				return hostLookupCgo
+			}
+			return hostLookupFiles
+		default:
+			return hostLookupCgo
+		}
+	}
+	if c.resolv != nil && c.resolv.unknownOpt {
+		return hostLookupCgo
+	}
+
+	hasDot := byteIndex(hostname, '.') != -1
+
+	// Canonicalize the hostname by removing any trailing dot.
+	if stringsHasSuffix(hostname, ".") {
+		hostname = hostname[:len(hostname)-1]
+	}
+	if stringsHasSuffixFold(hostname, ".local") {
+		// Per RFC 6762, the ".local" TLD is special.  And
+		// because Go's native resolver doesn't do mDNS or
+		// similar local resolution mechanisms, assume that
+		// libc might (via Avahi, etc) and use cgo.
+		return hostLookupCgo
+	}
+
+	nss := c.nss
+	srcs := nss.sources["hosts"]
+	// If /etc/nsswitch.conf doesn't exist or doesn't specify any
+	// sources for "hosts", assume Go's DNS will work fine.
+	if os.IsNotExist(nss.err) || (nss.err == nil && len(srcs) == 0) {
+		if c.goos == "solaris" {
+			// illumos defaults to "nis [NOTFOUND=return] files"
+			return hostLookupCgo
+		}
+		if c.goos == "linux" {
+			// glibc says the default is "dns [!UNAVAIL=return] files"
+			// http://www.gnu.org/software/libc/manual/html_node/Notes-on-NSS-Configuration-File.html.
+			return hostLookupDNSFiles
+		}
+		return hostLookupFilesDNS
+	}
+	if nss.err != nil {
+		// We failed to parse or open nsswitch.conf, so
+		// conservatively assume we should use cgo if it's
+		// available.
+		return hostLookupCgo
+	}
+
+	var mdnsSource, filesSource, dnsSource bool
+	var first string
+	for _, src := range srcs {
+		if src.source == "myhostname" {
+			if hasDot {
+				continue
+			}
+			return hostLookupCgo
+		}
+		if src.source == "files" || src.source == "dns" {
+			if !src.standardCriteria() {
+				return hostLookupCgo // non-standard; let libc deal with it.
+			}
+			if src.source == "files" {
+				filesSource = true
+			} else if src.source == "dns" {
+				dnsSource = true
+			}
+			if first == "" {
+				first = src.source
+			}
+			continue
+		}
+		if stringsHasPrefix(src.source, "mdns") {
+			// e.g. "mdns4", "mdns4_minimal"
+			// We already returned true before if it was *.local.
+			// libc wouldn't have found a hit on this anyway.
+			mdnsSource = true
+			continue
+		}
+		// Some source we don't know how to deal with.
+		return hostLookupCgo
+	}
+
+	// We don't parse mdns.allow files. They're rare. If one
+	// exists, it might list other TLDs (besides .local) or even
+	// '*', so just let libc deal with it.
+	if mdnsSource && c.hasMDNSAllow {
+		return hostLookupCgo
+	}
+
+	// Cases where Go can handle it without cgo and C thread
+	// overhead.
+	switch {
+	case filesSource && dnsSource:
+		if first == "files" {
+			return hostLookupFilesDNS
+		} else {
+			return hostLookupDNSFiles
+		}
+	case filesSource:
+		return hostLookupFiles
+	case dnsSource:
+		return hostLookupDNS
+	}
+
+	// Something weird. Let libc deal with it.
+	return hostLookupCgo
+}
diff --git a/src/net/conf_test.go b/src/net/conf_test.go
new file mode 100644
index 0000000..46e91bc
--- /dev/null
+++ b/src/net/conf_test.go
@@ -0,0 +1,245 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build darwin dragonfly freebsd linux netbsd openbsd solaris
+
+package net
+
+import (
+	"os"
+	"strings"
+	"testing"
+)
+
+type nssHostTest struct {
+	host string
+	want hostLookupOrder
+}
+
+func nssStr(s string) *nssConf { return parseNSSConf(strings.NewReader(s)) }
+
+func TestConfHostLookupOrder(t *testing.T) {
+	tests := []struct {
+		name      string
+		c         *conf
+		goos      string
+		hostTests []nssHostTest
+	}{
+		{
+			name: "force",
+			c: &conf{
+				forceCgoLookupHost: true,
+				nss:                nssStr("foo: bar"),
+			},
+			hostTests: []nssHostTest{
+				{"foo.local", hostLookupCgo},
+				{"google.com", hostLookupCgo},
+			},
+		},
+		{
+			name: "ubuntu_trusty_avahi",
+			c: &conf{
+				nss: nssStr("hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4"),
+			},
+			hostTests: []nssHostTest{
+				{"foo.local", hostLookupCgo},
+				{"foo.local.", hostLookupCgo},
+				{"foo.LOCAL", hostLookupCgo},
+				{"foo.LOCAL.", hostLookupCgo},
+				{"google.com", hostLookupFilesDNS},
+			},
+		},
+		{
+			name: "freebsdlinux_no_resolv_conf",
+			c: &conf{
+				goos: "freebsd",
+				nss:  nssStr("foo: bar"),
+			},
+			hostTests: []nssHostTest{{"google.com", hostLookupFilesDNS}},
+		},
+		// On OpenBSD, no resolv.conf means no DNS.
+		{
+			name: "openbsd_no_resolv_conf",
+			c: &conf{
+				goos: "openbsd",
+			},
+			hostTests: []nssHostTest{{"google.com", hostLookupFiles}},
+		},
+		{
+			name: "solaris_no_nsswitch",
+			c: &conf{
+				goos: "solaris",
+				nss:  &nssConf{err: os.ErrNotExist},
+			},
+			hostTests: []nssHostTest{{"google.com", hostLookupCgo}},
+		},
+		{
+			name: "openbsd_lookup_bind_file",
+			c: &conf{
+				goos:   "openbsd",
+				resolv: &dnsConfig{lookup: []string{"bind", "file"}},
+			},
+			hostTests: []nssHostTest{
+				{"google.com", hostLookupDNSFiles},
+				{"foo.local", hostLookupDNSFiles},
+			},
+		},
+		{
+			name: "openbsd_lookup_file_bind",
+			c: &conf{
+				goos:   "openbsd",
+				resolv: &dnsConfig{lookup: []string{"file", "bind"}},
+			},
+			hostTests: []nssHostTest{{"google.com", hostLookupFilesDNS}},
+		},
+		{
+			name: "openbsd_lookup_bind",
+			c: &conf{
+				goos:   "openbsd",
+				resolv: &dnsConfig{lookup: []string{"bind"}},
+			},
+			hostTests: []nssHostTest{{"google.com", hostLookupDNS}},
+		},
+		{
+			name: "openbsd_lookup_file",
+			c: &conf{
+				goos:   "openbsd",
+				resolv: &dnsConfig{lookup: []string{"file"}},
+			},
+			hostTests: []nssHostTest{{"google.com", hostLookupFiles}},
+		},
+		{
+			name: "openbsd_lookup_yp",
+			c: &conf{
+				goos:   "openbsd",
+				resolv: &dnsConfig{lookup: []string{"file", "bind", "yp"}},
+			},
+			hostTests: []nssHostTest{{"google.com", hostLookupCgo}},
+		},
+		{
+			name: "openbsd_lookup_two",
+			c: &conf{
+				goos:   "openbsd",
+				resolv: &dnsConfig{lookup: []string{"file", "foo"}},
+			},
+			hostTests: []nssHostTest{{"google.com", hostLookupCgo}},
+		},
+		{
+			name: "openbsd_lookup_empty",
+			c: &conf{
+				goos:   "openbsd",
+				resolv: &dnsConfig{lookup: nil},
+			},
+			hostTests: []nssHostTest{{"google.com", hostLookupDNSFiles}},
+		},
+		// glibc lacking an nsswitch.conf, per
+		// http://www.gnu.org/software/libc/manual/html_node/Notes-on-NSS-Configuration-File.html
+		{
+			name: "linux_no_nsswitch.conf",
+			c: &conf{
+				goos: "linux",
+				nss:  &nssConf{err: os.ErrNotExist},
+			},
+			hostTests: []nssHostTest{{"google.com", hostLookupDNSFiles}},
+		},
+		{
+			name: "files_mdns_dns",
+			c:    &conf{nss: nssStr("hosts: files mdns dns")},
+			hostTests: []nssHostTest{
+				{"x.com", hostLookupFilesDNS},
+				{"x.local", hostLookupCgo},
+			},
+		},
+		{
+			name: "dns_special_hostnames",
+			c:    &conf{nss: nssStr("hosts: dns")},
+			hostTests: []nssHostTest{
+				{"x.com", hostLookupDNS},
+				{"x\\.com", hostLookupCgo},     // punt on weird glibc escape
+				{"foo.com%en0", hostLookupCgo}, // and IPv6 zones
+			},
+		},
+		{
+			name: "mdns_allow",
+			c: &conf{
+				nss:          nssStr("hosts: files mdns dns"),
+				hasMDNSAllow: true,
+			},
+			hostTests: []nssHostTest{
+				{"x.com", hostLookupCgo},
+				{"x.local", hostLookupCgo},
+			},
+		},
+		{
+			name: "files_dns",
+			c:    &conf{nss: nssStr("hosts: files dns")},
+			hostTests: []nssHostTest{
+				{"x.com", hostLookupFilesDNS},
+				{"x", hostLookupFilesDNS},
+				{"x.local", hostLookupCgo},
+			},
+		},
+		{
+			name: "dns_files",
+			c:    &conf{nss: nssStr("hosts: dns files")},
+			hostTests: []nssHostTest{
+				{"x.com", hostLookupDNSFiles},
+				{"x", hostLookupDNSFiles},
+				{"x.local", hostLookupCgo},
+			},
+		},
+		{
+			name: "something_custom",
+			c:    &conf{nss: nssStr("hosts: dns files something_custom")},
+			hostTests: []nssHostTest{
+				{"x.com", hostLookupCgo},
+			},
+		},
+		{
+			name: "myhostname",
+			c:    &conf{nss: nssStr("hosts: files dns myhostname")},
+			hostTests: []nssHostTest{
+				{"x.com", hostLookupFilesDNS},
+				{"somehostname", hostLookupCgo},
+			},
+		},
+		{
+			name: "ubuntu14.04.02",
+			c:    &conf{nss: nssStr("hosts: files myhostname mdns4_minimal [NOTFOUND=return] dns mdns4")},
+			hostTests: []nssHostTest{
+				{"x.com", hostLookupFilesDNS},
+				{"somehostname", hostLookupCgo},
+			},
+		},
+		// Debian Squeeze is just "dns,files", but lists all
+		// the default criteria for dns, but then has a
+		// non-standard but redundant notfound=return for the
+		// files.
+		{
+			name: "debian_squeeze",
+			c:    &conf{nss: nssStr("hosts: dns [success=return notfound=continue unavail=continue tryagain=continue] files [notfound=return]")},
+			hostTests: []nssHostTest{
+				{"x.com", hostLookupDNSFiles},
+				{"somehostname", hostLookupDNSFiles},
+			},
+		},
+		{
+			name: "resolv.conf-unknown",
+			c: &conf{
+				nss:    nssStr("foo: bar"),
+				resolv: &dnsConfig{unknownOpt: true},
+			},
+			hostTests: []nssHostTest{{"google.com", hostLookupCgo}},
+		},
+	}
+	for _, tt := range tests {
+		for _, ht := range tt.hostTests {
+			gotOrder := tt.c.hostLookupOrder(ht.host)
+			if gotOrder != ht.want {
+				t.Errorf("%s: useCgoLookupHost(%q) = %v; want %v", tt.name, ht.host, gotOrder, ht.want)
+			}
+		}
+	}
+
+}
diff --git a/src/net/dnsclient_unix.go b/src/net/dnsclient_unix.go
index 3dd22f2..55647eb 100644
--- a/src/net/dnsclient_unix.go
+++ b/src/net/dnsclient_unix.go
@@ -20,6 +20,7 @@
 	"io"
 	"math/rand"
 	"os"
+	"strconv"
 	"sync"
 	"time"
 )
@@ -332,6 +333,35 @@
 	return
 }
 
+// hostLookupOrder specifies the order of LookupHost lookup strategies.
+// It is basically a simplified representation of nsswitch.conf.
+// "files" means /etc/hosts.
+type hostLookupOrder int
+
+const (
+	// hostLookupCgo means defer to cgo.
+	hostLookupCgo      hostLookupOrder = iota
+	hostLookupFilesDNS                 // files first
+	hostLookupDNSFiles                 // dns first
+	hostLookupFiles                    // only files
+	hostLookupDNS                      // only DNS
+)
+
+var lookupOrderName = map[hostLookupOrder]string{
+	hostLookupCgo:      "cgo",
+	hostLookupFilesDNS: "files,dns",
+	hostLookupDNSFiles: "dns,files",
+	hostLookupFiles:    "files",
+	hostLookupDNS:      "dns",
+}
+
+func (o hostLookupOrder) String() string {
+	if s, ok := lookupOrderName[o]; ok {
+		return s
+	}
+	return "hostLookupOrder=" + strconv.Itoa(int(o)) + "??"
+}
+
 // goLookupHost is the native Go implementation of LookupHost.
 // Used only if cgoLookupHost refuses to handle the request
 // (that is, only if cgoLookupHost is the stub in cgo_stub.go).
@@ -339,12 +369,18 @@
 // depending on our lookup code, so that Go and C get the same
 // answers.
 func goLookupHost(name string) (addrs []string, err error) {
-	// Use entries from /etc/hosts if they match.
-	addrs = lookupStaticHost(name)
-	if len(addrs) > 0 {
-		return
+	return goLookupHostOrder(name, hostLookupFilesDNS)
+}
+
+func goLookupHostOrder(name string, order hostLookupOrder) (addrs []string, err error) {
+	if order == hostLookupFilesDNS || order == hostLookupFiles {
+		// Use entries from /etc/hosts if they match.
+		addrs = lookupStaticHost(name)
+		if len(addrs) > 0 || order == hostLookupFiles {
+			return
+		}
 	}
-	ips, err := goLookupIP(name)
+	ips, err := goLookupIPOrder(name, order)
 	if err != nil {
 		return
 	}
@@ -355,25 +391,30 @@
 	return
 }
 
+// lookup entries from /etc/hosts
+func goLookupIPFiles(name string) (addrs []IPAddr) {
+	for _, haddr := range lookupStaticHost(name) {
+		haddr, zone := splitHostZone(haddr)
+		if ip := ParseIP(haddr); ip != nil {
+			addr := IPAddr{IP: ip, Zone: zone}
+			addrs = append(addrs, addr)
+		}
+	}
+	return
+}
+
 // goLookupIP is the native Go implementation of LookupIP.
 // Used only if cgoLookupIP refuses to handle the request
 // (that is, only if cgoLookupIP is the stub in cgo_stub.go).
-// Normally we let cgo use the C library resolver instead of
-// depending on our lookup code, so that Go and C get the same
-// answers.
 func goLookupIP(name string) (addrs []IPAddr, err error) {
-	// Use entries from /etc/hosts if possible.
-	haddrs := lookupStaticHost(name)
-	if len(haddrs) > 0 {
-		for _, haddr := range haddrs {
-			haddr, zone := splitHostZone(haddr)
-			if ip := ParseIP(haddr); ip != nil {
-				addr := IPAddr{IP: ip, Zone: zone}
-				addrs = append(addrs, addr)
-			}
-		}
-		if len(addrs) > 0 {
-			return
+	return goLookupIPOrder(name, hostLookupFilesDNS)
+}
+
+func goLookupIPOrder(name string, order hostLookupOrder) (addrs []IPAddr, err error) {
+	if order == hostLookupFilesDNS || order == hostLookupFiles {
+		addrs = goLookupIPFiles(name)
+		if len(addrs) > 0 || order == hostLookupFiles {
+			return addrs, nil
 		}
 	}
 	type racer struct {
@@ -409,8 +450,13 @@
 			}
 		}
 	}
-	if len(addrs) == 0 && lastErr != nil {
-		return nil, lastErr
+	if len(addrs) == 0 {
+		if lastErr != nil {
+			return nil, lastErr
+		}
+		if order == hostLookupDNSFiles {
+			addrs = goLookupIPFiles(name)
+		}
 	}
 	return addrs, nil
 }
diff --git a/src/net/dnsconfig_unix.go b/src/net/dnsconfig_unix.go
index 66ab7c4..abaef7b 100644
--- a/src/net/dnsconfig_unix.go
+++ b/src/net/dnsconfig_unix.go
@@ -9,12 +9,14 @@
 package net
 
 type dnsConfig struct {
-	servers  []string // servers to use
-	search   []string // suffixes to append to local name
-	ndots    int      // number of dots in name to trigger absolute lookup
-	timeout  int      // seconds before giving up on packet
-	attempts int      // lost packets before giving up on server
-	rotate   bool     // round robin among servers
+	servers    []string // servers to use
+	search     []string // suffixes to append to local name
+	ndots      int      // number of dots in name to trigger absolute lookup
+	timeout    int      // seconds before giving up on packet
+	attempts   int      // lost packets before giving up on server
+	rotate     bool     // round robin among servers
+	unknownOpt bool     // anything unknown was encountered
+	lookup     []string // OpenBSD top-level database "lookup" order
 }
 
 // See resolv.conf(5) on a Linux machine.
@@ -32,6 +34,10 @@
 		attempts: 2,
 	}
 	for line, ok := file.readLine(); ok; line, ok = file.readLine() {
+		if len(line) > 0 && (line[0] == ';' || line[0] == '#') {
+			// comment.
+			continue
+		}
 		f := getFields(line)
 		if len(f) < 1 {
 			continue
@@ -61,8 +67,7 @@
 			}
 
 		case "options": // magic options
-			for i := 1; i < len(f); i++ {
-				s := f[i]
+			for _, s := range f[1:] {
 				switch {
 				case hasPrefix(s, "ndots:"):
 					n, _, _ := dtoi(s, 6)
@@ -84,8 +89,19 @@
 					conf.attempts = n
 				case s == "rotate":
 					conf.rotate = true
+				default:
+					conf.unknownOpt = true
 				}
 			}
+
+		case "lookup":
+			// OpenBSD option:
+			// http://www.openbsd.org/cgi-bin/man.cgi/OpenBSD-current/man5/resolv.conf.5
+			// "the legal space-separated values are: bind, file, yp"
+			conf.lookup = f[1:]
+
+		default:
+			conf.unknownOpt = true
 		}
 	}
 	return conf, nil
diff --git a/src/net/dnsconfig_unix_test.go b/src/net/dnsconfig_unix_test.go
index 94fb0c3..f4b1185 100644
--- a/src/net/dnsconfig_unix_test.go
+++ b/src/net/dnsconfig_unix_test.go
@@ -13,22 +13,23 @@
 
 var dnsReadConfigTests = []struct {
 	name string
-	conf dnsConfig
+	want *dnsConfig
 }{
 	{
 		name: "testdata/resolv.conf",
-		conf: dnsConfig{
-			servers:  []string{"8.8.8.8", "2001:4860:4860::8888", "fe80::1%lo0"},
-			search:   []string{"localdomain"},
-			ndots:    5,
-			timeout:  10,
-			attempts: 3,
-			rotate:   true,
+		want: &dnsConfig{
+			servers:    []string{"8.8.8.8", "2001:4860:4860::8888", "fe80::1%lo0"},
+			search:     []string{"localdomain"},
+			ndots:      5,
+			timeout:    10,
+			attempts:   3,
+			rotate:     true,
+			unknownOpt: true, // the "options attempts 3" line
 		},
 	},
 	{
 		name: "testdata/domain-resolv.conf",
-		conf: dnsConfig{
+		want: &dnsConfig{
 			servers:  []string{"8.8.8.8"},
 			search:   []string{"localdomain"},
 			ndots:    1,
@@ -38,7 +39,7 @@
 	},
 	{
 		name: "testdata/search-resolv.conf",
-		conf: dnsConfig{
+		want: &dnsConfig{
 			servers:  []string{"8.8.8.8"},
 			search:   []string{"test", "invalid"},
 			ndots:    1,
@@ -48,12 +49,23 @@
 	},
 	{
 		name: "testdata/empty-resolv.conf",
-		conf: dnsConfig{
+		want: &dnsConfig{
 			ndots:    1,
 			timeout:  5,
 			attempts: 2,
 		},
 	},
+	{
+		name: "testdata/openbsd-resolv.conf",
+		want: &dnsConfig{
+			ndots:    1,
+			timeout:  5,
+			attempts: 2,
+			lookup:   []string{"file", "bind"},
+			servers:  []string{"169.254.169.254", "10.240.0.1"},
+			search:   []string{"c.symbolic-datum-552.internal."},
+		},
+	},
 }
 
 func TestDNSReadConfig(t *testing.T) {
@@ -62,8 +74,8 @@
 		if err != nil {
 			t.Fatal(err)
 		}
-		if !reflect.DeepEqual(conf, &tt.conf) {
-			t.Errorf("got %v; want %v", conf, &tt.conf)
+		if !reflect.DeepEqual(conf, tt.want) {
+			t.Errorf("%s:\n got: %+v\nwant: %+v", tt.name, conf, tt.want)
 		}
 	}
 }
diff --git a/src/net/lookup_unix.go b/src/net/lookup_unix.go
index f9c2393..6484414 100644
--- a/src/net/lookup_unix.go
+++ b/src/net/lookup_unix.go
@@ -49,20 +49,28 @@
 	return proto, nil
 }
 
-func lookupHost(host string) ([]string, error) {
-	addrs, err, ok := cgoLookupHost(host)
-	if !ok {
-		addrs, err = goLookupHost(host)
+func lookupHost(host string) (addrs []string, err error) {
+	order := systemConf().hostLookupOrder(host)
+	if order == hostLookupCgo {
+		if addrs, err, ok := cgoLookupHost(host); ok {
+			return addrs, err
+		}
+		// cgo not available (or netgo); fall back to Go's DNS resolver
+		order = hostLookupFilesDNS
 	}
-	return addrs, err
+	return goLookupHostOrder(host, order)
 }
 
-func lookupIP(host string) ([]IPAddr, error) {
-	addrs, err, ok := cgoLookupIP(host)
-	if !ok {
-		addrs, err = goLookupIP(host)
+func lookupIP(host string) (addrs []IPAddr, err error) {
+	order := systemConf().hostLookupOrder(host)
+	if order == hostLookupCgo {
+		if addrs, err, ok := cgoLookupIP(host); ok {
+			return addrs, err
+		}
+		// cgo not available (or netgo); fall back to Go's DNS resolver
+		order = hostLookupFilesDNS
 	}
-	return addrs, err
+	return goLookupIPOrder(host, order)
 }
 
 func lookupPort(network, service string) (int, error) {
diff --git a/src/net/nss.go b/src/net/nss.go
new file mode 100644
index 0000000..08c3e6a
--- /dev/null
+++ b/src/net/nss.go
@@ -0,0 +1,159 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build darwin dragonfly freebsd linux netbsd openbsd solaris
+
+package net
+
+import (
+	"errors"
+	"io"
+	"os"
+)
+
+// nssConf represents the state of the machine's /etc/nsswitch.conf file.
+type nssConf struct {
+	err     error                  // any error encountered opening or parsing the file
+	sources map[string][]nssSource // keyed by database (e.g. "hosts")
+}
+
+type nssSource struct {
+	source   string // e.g. "compat", "files", "mdns4_minimal"
+	criteria []nssCriterion
+}
+
+// standardCriteria reports all specified criteria have the default
+// status actions.
+func (s nssSource) standardCriteria() bool {
+	for i, crit := range s.criteria {
+		if !crit.standardStatusAction(i == len(s.criteria)-1) {
+			return false
+		}
+	}
+	return true
+}
+
+// nssCriterion is the parsed structure of one of the criteria in brackets
+// after an NSS source name.
+type nssCriterion struct {
+	negate bool   // if "!" was present
+	status string // e.g. "success", "unavail" (lowercase)
+	action string // e.g. "return", "continue" (lowercase)
+}
+
+// standardStatusAction reports whether c is equivalent to not
+// specifying the criterion at all. last is whether this criteria is the
+// last in the list.
+func (c nssCriterion) standardStatusAction(last bool) bool {
+	if c.negate {
+		return false
+	}
+	var def string
+	switch c.status {
+	case "success":
+		def = "return"
+	case "notfound", "unavail", "tryagain":
+		def = "continue"
+	default:
+		// Unknown status
+		return false
+	}
+	if last && c.action == "return" {
+		return true
+	}
+	return c.action == def
+}
+
+func parseNSSConfFile(file string) *nssConf {
+	f, err := os.Open(file)
+	if err != nil {
+		return &nssConf{err: err}
+	}
+	defer f.Close()
+	return parseNSSConf(f)
+}
+
+func parseNSSConf(r io.Reader) *nssConf {
+	slurp, err := readFull(r)
+	if err != nil {
+		return &nssConf{err: err}
+	}
+	conf := new(nssConf)
+	conf.err = foreachLine(slurp, func(line []byte) error {
+		line = trimSpace(removeComment(line))
+		if len(line) == 0 {
+			return nil
+		}
+		colon := bytesIndexByte(line, ':')
+		if colon == -1 {
+			return errors.New("no colon on line")
+		}
+		db := string(trimSpace(line[:colon]))
+		srcs := line[colon+1:]
+		for {
+			srcs = trimSpace(srcs)
+			if len(srcs) == 0 {
+				break
+			}
+			sp := bytesIndexByte(srcs, ' ')
+			var src string
+			if sp == -1 {
+				src = string(srcs)
+				srcs = nil // done
+			} else {
+				src = string(srcs[:sp])
+				srcs = trimSpace(srcs[sp+1:])
+			}
+			var criteria []nssCriterion
+			// See if there's a criteria block in brackets.
+			if len(srcs) > 0 && srcs[0] == '[' {
+				bclose := bytesIndexByte(srcs, ']')
+				if bclose == -1 {
+					return errors.New("unclosed criterion bracket")
+				}
+				var err error
+				criteria, err = parseCriteria(srcs[1:bclose])
+				if err != nil {
+					return errors.New("invalid criteria: " + string(srcs[1:bclose]))
+				}
+				srcs = srcs[bclose+1:]
+			}
+			if conf.sources == nil {
+				conf.sources = make(map[string][]nssSource)
+			}
+			conf.sources[db] = append(conf.sources[db], nssSource{
+				source:   src,
+				criteria: criteria,
+			})
+		}
+		return nil
+	})
+	return conf
+}
+
+// parses "foo=bar !foo=bar"
+func parseCriteria(x []byte) (c []nssCriterion, err error) {
+	err = foreachField(x, func(f []byte) error {
+		not := false
+		if len(f) > 0 && f[0] == '!' {
+			not = true
+			f = f[1:]
+		}
+		if len(f) < 3 {
+			return errors.New("criterion too short")
+		}
+		eq := bytesIndexByte(f, '=')
+		if eq == -1 {
+			return errors.New("criterion lacks equal sign")
+		}
+		lowerASCIIBytes(f)
+		c = append(c, nssCriterion{
+			negate: not,
+			status: string(f[:eq]),
+			action: string(f[eq+1:]),
+		})
+		return nil
+	})
+	return
+}
diff --git a/src/net/nss_test.go b/src/net/nss_test.go
new file mode 100644
index 0000000..371deb5
--- /dev/null
+++ b/src/net/nss_test.go
@@ -0,0 +1,169 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build darwin dragonfly freebsd linux netbsd openbsd solaris
+
+package net
+
+import (
+	"reflect"
+	"strings"
+	"testing"
+)
+
+const ubuntuTrustyAvahi = `# /etc/nsswitch.conf
+#
+# Example configuration of GNU Name Service Switch functionality.
+# If you have the libc-doc-reference' and nfo' packages installed, try:
+# nfo libc "Name Service Switch"' for information about this file.
+
+passwd:         compat
+group:          compat
+shadow:         compat
+
+hosts:          files mdns4_minimal [NOTFOUND=return] dns mdns4
+networks:       files
+
+protocols:      db files
+services:       db files
+ethers:         db files
+rpc:            db files
+
+netgroup:       nis
+`
+
+func TestParseNSSConf(t *testing.T) {
+	tests := []struct {
+		name string
+		in   string
+		want *nssConf
+	}{
+		{
+			name: "no_newline",
+			in:   "foo: a b",
+			want: &nssConf{
+				sources: map[string][]nssSource{
+					"foo": {{source: "a"}, {source: "b"}},
+				},
+			},
+		},
+		{
+			name: "newline",
+			in:   "foo: a b\n",
+			want: &nssConf{
+				sources: map[string][]nssSource{
+					"foo": {{source: "a"}, {source: "b"}},
+				},
+			},
+		},
+		{
+			name: "whitespace",
+			in:   "   foo:a    b    \n",
+			want: &nssConf{
+				sources: map[string][]nssSource{
+					"foo": {{source: "a"}, {source: "b"}},
+				},
+			},
+		},
+		{
+			name: "comment1",
+			in:   "   foo:a    b#c\n",
+			want: &nssConf{
+				sources: map[string][]nssSource{
+					"foo": {{source: "a"}, {source: "b"}},
+				},
+			},
+		},
+		{
+			name: "comment2",
+			in:   "   foo:a    b #c \n",
+			want: &nssConf{
+				sources: map[string][]nssSource{
+					"foo": {{source: "a"}, {source: "b"}},
+				},
+			},
+		},
+		{
+			name: "crit",
+			in:   "   foo:a    b [!a=b    X=Y ] c#d \n",
+			want: &nssConf{
+				sources: map[string][]nssSource{
+					"foo": {
+						{source: "a"},
+						{
+							source: "b",
+							criteria: []nssCriterion{
+								{
+									negate: true,
+									status: "a",
+									action: "b",
+								},
+								{
+									status: "x",
+									action: "y",
+								},
+							},
+						},
+						{source: "c"},
+					},
+				},
+			},
+		},
+
+		// Ubuntu Trusty w/ avahi-daemon, libavahi-* etc installed.
+		{
+			name: "ubuntu_trusty_avahi",
+			in:   ubuntuTrustyAvahi,
+			want: &nssConf{
+				sources: map[string][]nssSource{
+					"passwd": {{source: "compat"}},
+					"group":  {{source: "compat"}},
+					"shadow": {{source: "compat"}},
+					"hosts": {
+						{source: "files"},
+						{
+							source: "mdns4_minimal",
+							criteria: []nssCriterion{
+								{
+									negate: false,
+									status: "notfound",
+									action: "return",
+								},
+							},
+						},
+						{source: "dns"},
+						{source: "mdns4"},
+					},
+					"networks": {{source: "files"}},
+					"protocols": {
+						{source: "db"},
+						{source: "files"},
+					},
+					"services": {
+						{source: "db"},
+						{source: "files"},
+					},
+					"ethers": {
+						{source: "db"},
+						{source: "files"},
+					},
+					"rpc": {
+						{source: "db"},
+						{source: "files"},
+					},
+					"netgroup": {
+						{source: "nis"},
+					},
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		gotConf := parseNSSConf(strings.NewReader(tt.in))
+		if !reflect.DeepEqual(gotConf, tt.want) {
+			t.Errorf("%s: mismatch\n got %#v\nwant %#v", tt.name, gotConf, tt.want)
+		}
+	}
+}
diff --git a/src/net/parse.go b/src/net/parse.go
index ad901ff..5b834e6 100644
--- a/src/net/parse.go
+++ b/src/net/parse.go
@@ -232,3 +232,132 @@
 	}
 	return i
 }
+
+// lowerASCIIBytes makes x ASCII lowercase in-place.
+func lowerASCIIBytes(x []byte) {
+	for i, b := range x {
+		if 'A' <= b && b <= 'Z' {
+			x[i] += 'a' - 'A'
+		}
+	}
+}
+
+// lowerASCII returns the ASCII lowercase version of b.
+func lowerASCII(b byte) byte {
+	if 'A' <= b && b <= 'Z' {
+		return b + ('a' - 'A')
+	}
+	return b
+}
+
+// trimSpace returns x without any leading or trailing ASCII whitespace.
+func trimSpace(x []byte) []byte {
+	for len(x) > 0 && isSpace(x[0]) {
+		x = x[1:]
+	}
+	for len(x) > 0 && isSpace(x[len(x)-1]) {
+		x = x[:len(x)-1]
+	}
+	return x
+}
+
+// isSpace reports whether b is an ASCII space character.
+func isSpace(b byte) bool {
+	return b == ' ' || b == '\t' || b == '\n' || b == '\r'
+}
+
+// removeComment returns line, removing any '#' byte and any following
+// bytes.
+func removeComment(line []byte) []byte {
+	if i := bytesIndexByte(line, '#'); i != -1 {
+		return line[:i]
+	}
+	return line
+}
+
+// foreachLine runs fn on each line of x.
+// Each line (except for possibly the last) ends in '\n'.
+// It returns the first non-nil error returned by fn.
+func foreachLine(x []byte, fn func(line []byte) error) error {
+	for len(x) > 0 {
+		nl := bytesIndexByte(x, '\n')
+		if nl == -1 {
+			return fn(x)
+		}
+		line := x[:nl+1]
+		x = x[nl+1:]
+		if err := fn(line); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// foreachField runs fn on each non-empty run of non-space bytes in x.
+// It returns the first non-nil error returned by fn.
+func foreachField(x []byte, fn func(field []byte) error) error {
+	x = trimSpace(x)
+	for len(x) > 0 {
+		sp := bytesIndexByte(x, ' ')
+		if sp == -1 {
+			return fn(x)
+		}
+		if field := trimSpace(x[:sp]); len(field) > 0 {
+			if err := fn(field); err != nil {
+				return err
+			}
+		}
+		x = trimSpace(x[sp+1:])
+	}
+	return nil
+}
+
+// bytesIndexByte is bytes.IndexByte. It returns the index of the
+// first instance of c in s, or -1 if c is not present in s.
+func bytesIndexByte(s []byte, c byte) int {
+	for i, b := range s {
+		if b == c {
+			return i
+		}
+	}
+	return -1
+}
+
+// stringsHasSuffix is strings.HasSuffix. It reports whether s ends in
+// suffix.
+func stringsHasSuffix(s, suffix string) bool {
+	return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
+}
+
+// stringsHasSuffixFold reports whether s ends in suffix,
+// ASCII-case-insensitively.
+func stringsHasSuffixFold(s, suffix string) bool {
+	if len(suffix) > len(s) {
+		return false
+	}
+	for i := 0; i < len(suffix); i++ {
+		if lowerASCII(suffix[i]) != lowerASCII(s[len(s)-len(suffix)+i]) {
+			return false
+		}
+	}
+	return true
+}
+
+// stringsHasPrefix is strings.HasPrefix. It reports whether s begins with prefix.
+func stringsHasPrefix(s, prefix string) bool {
+	return len(s) >= len(prefix) && s[:len(prefix)] == prefix
+}
+
+func readFull(r io.Reader) (all []byte, err error) {
+	buf := make([]byte, 1024)
+	for {
+		n, err := r.Read(buf)
+		all = append(all, buf[:n]...)
+		if err == io.EOF {
+			return all, nil
+		}
+		if err != nil {
+			return nil, err
+		}
+	}
+}
diff --git a/src/net/testdata/openbsd-resolv.conf b/src/net/testdata/openbsd-resolv.conf
new file mode 100644
index 0000000..8281a91
--- /dev/null
+++ b/src/net/testdata/openbsd-resolv.conf
@@ -0,0 +1,5 @@
+# Generated by vio0 dhclient
+search c.symbolic-datum-552.internal.
+nameserver 169.254.169.254
+nameserver 10.240.0.1
+lookup file bind