Commit 4a0ba7aa authored by Brad Fitzpatrick's avatar Brad Fitzpatrick

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/8945Reviewed-by: 's avatarIan Lance Taylor <iant@golang.org>
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
parent c2312280
// 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
}
// 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)
}
}
}
}
......@@ -20,6 +20,7 @@ import (
"io"
"math/rand"
"os"
"strconv"
"sync"
"time"
)
......@@ -332,6 +333,35 @@ func lookup(name string, qtype uint16) (cname string, rrs []dnsRR, err error) {
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 @@ func lookup(name string, qtype uint16) (cname string, rrs []dnsRR, err error) {
// 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 @@ func goLookupHost(name string) (addrs []string, err error) {
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 @@ func goLookupIP(name string) (addrs []IPAddr, err error) {
}
}
}
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
}
......
......@@ -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 @@ func dnsReadConfig(filename string) (*dnsConfig, error) {
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 @@ func dnsReadConfig(filename string) (*dnsConfig, error) {
}
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 @@ func dnsReadConfig(filename string) (*dnsConfig, error) {
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
......
......@@ -13,22 +13,23 @@ import (
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 @@ var dnsReadConfigTests = []struct {
},
{
name: "testdata/search-resolv.conf",
conf: dnsConfig{
want: &dnsConfig{
servers: []string{"8.8.8.8"},
search: []string{"test", "invalid"},
ndots: 1,
......@@ -48,10 +49,21 @@ var dnsReadConfigTests = []struct {
},
{
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."},
},
},
}
......@@ -62,8 +74,8 @@ func TestDNSReadConfig(t *testing.T) {
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)
}
}
}
......@@ -49,20 +49,28 @@ func lookupProtocol(name string) (int, error) {
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) {
......
// 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
}
// 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)
}
}
}
......@@ -232,3 +232,132 @@ func last(s string, b byte) int {
}
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
}
}
}
# Generated by vio0 dhclient
search c.symbolic-datum-552.internal.
nameserver 169.254.169.254
nameserver 10.240.0.1
lookup file bind
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