1
// Copyright 2016 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
11
"github.com/juju/errors"
14
// DNSConfig holds a list of DNS nameserver addresses and default search
16
type DNSConfig struct {
18
SearchDomains []string
21
// ParseResolvConf parses a resolv.conf(5) file at the given path (usually
22
// "/etc/resolv.conf"), if present. Returns the values of any 'nameserver'
23
// stanzas, and the last 'search' stanza found. Values in the result will appear
24
// in the order found, including duplicates. Parsing errors will be returned in
27
// 1. if a 'nameserver' or 'search' without a value is found;
28
// 2. 'nameserver' with more than one value (trailing comments starting with '#'
29
// or ';' after the value are allowed).
30
// 3. if any value containing '#' or ';' (e.g. 'nameserver 8.8.8.8#bad'), because
31
// values and comments following them must be separated by whitespace.
33
// No error is returned if the file is missing. See resolv.conf(5) man page for
35
func ParseResolvConf(path string) (*DNSConfig, error) {
36
file, err := os.Open(path)
37
if os.IsNotExist(err) {
38
logger.Debugf("%q does not exist - not parsing", path)
40
} else if err != nil {
41
return nil, errors.Trace(err)
47
searchDomains []string
49
scanner := bufio.NewScanner(file)
52
line := scanner.Text()
55
values, err := parseResolvStanza(line, "nameserver")
57
return nil, errors.Annotatef(err, "parsing %q, line %d", path, lineNum)
60
if numValues := len(values); numValues > 1 {
61
return nil, errors.Errorf(
62
"parsing %q, line %d: one value expected for \"nameserver\", got %d",
63
path, lineNum, numValues,
65
} else if numValues == 1 {
66
nameservers = append(nameservers, values[0])
70
values, err = parseResolvStanza(line, "search")
72
return nil, errors.Annotatef(err, "parsing %q, line %d", path, lineNum)
76
// Last 'search' found wins.
77
searchDomains = values
81
if err := scanner.Err(); err != nil {
82
return nil, errors.Annotatef(err, "reading %q", path)
86
Nameservers: NewAddresses(nameservers...),
87
SearchDomains: searchDomains,
91
// parseResolvStanza parses a single line from a resolv.conf(5) file, beginning
92
// with the given stanza ('nameserver' or 'search' ). If the line does not
93
// contain the stanza, no results and no error is returned. Leading and trailing
94
// whitespace is removed first, then lines starting with ";" or "#" are treated
98
// parseResolvStanza(` # nothing ;to see here`, "doesn't matter")
99
// will return (nil, nil) - comments and whitespace are ignored, nothing left.
101
// parseResolvStanza(` nameserver ns1.example.com # preferred`, "nameserver")
102
// will return ([]string{"ns1.example.com"}, nil).
104
// parseResolvStanza(`search ;; bad: no value`, "search")
105
// will return (nil, err: `"search": required value(s) missing`)
107
// parseResolvStanza(`search foo bar foo foo.bar bar.foo ;; try all`, "search")
108
// will return ([]string("foo", "bar", "foo", "foo.bar", "bar.foo"}, nil)
110
// parseResolvStanza(`search foo#bad comment`, "nameserver")
111
// will return (nil, nil) - line does not start with "nameserver".
113
// parseResolvStanza(`search foo#bad comment`, "search")
114
// will return (nil, err: `"search": invalid value "foo#bad"`) - no whitespace
115
// between the value "foo" and the following comment "#bad comment".
116
func parseResolvStanza(line, stanza string) ([]string, error) {
117
const commentChars = ";#"
118
isComment := func(s string) bool {
119
return strings.IndexAny(s, commentChars) == 0
122
line = strings.TrimSpace(line)
123
fields := strings.Fields(line)
124
noFields := len(fields) == 0 // line contains only whitespace
126
if isComment(line) || noFields || fields[0] != stanza {
127
// Lines starting with ';' or '#' are comments and are ignored. Empty
128
// lines and those not starting with stanza are ignored.
132
// Mostly for convenience, comments starting with ';' or '#' after a value
133
// are allowed and ignored, assuming there's whitespace between the value
134
// and the comment (e.g. 'search foo #bar' is OK, but 'search foo#bar'
136
var parsedValues []string
137
rawValues := fields[1:] // skip the stanza itself
138
for _, value := range rawValues {
139
if isComment(value) {
140
// We're done parsing as the rest of the line is still part of the
145
if strings.ContainsAny(value, commentChars) {
146
// This will catch cases like 'nameserver 8.8.8.8#foo', because
147
// fields[1] will be '8.8.8.8#foo'.
148
return nil, errors.Errorf("%q: invalid value %q", stanza, value)
151
parsedValues = append(parsedValues, value)
154
// resolv.conf(5) states that to be recognized as valid, the line must begin
155
// with the stanza, followed by whitespace, then at least one value (for
156
// 'nameserver', more values separated by whitespace are allowed for
158
if len(parsedValues) == 0 {
159
return nil, errors.Errorf("%q: required value(s) missing", stanza)
162
return parsedValues, nil