pkg/networkfs/resolvconf/resolvconf.go
Phil Estes 4b79ea2a3c Clean up localhost resolv logic and add IPv6 support to regexp
Addresses #5811

This cleans up an error in the logic which removes localhost resolvers
from the host resolv.conf at container creation start time. Specifically
when the determination is made if any nameservers are left after
removing localhost resolvers, it was using a string match on the word
"nameserver", which could have been anywhere (including commented out)
leading to incorrect situations where no nameservers were left but the
default ones were not added.

This also adds some complexity to the regular expressions for finding
nameservers in general, as well as matching on localhost resolvers due
to the recent addition of IPv6 support.  Because of IPv6 support now
available in the Docker daemon, the resolvconf code is now aware of
IPv6 enable/disable state and uses that for both filter/cleaning of
nameservers as well as adding default Google DNS (IPv4 only vs. IPv4
and IPv6 if IPv6 enabled).  For all these changes, tests have been
added/strengthened to test these additional capabilities.

Docker-DCO-1.1-Signed-off-by: Phil Estes <estesp@linux.vnet.ibm.com> (github: estesp)
2015-01-19 19:35:55 -05:00

181 lines
6.1 KiB
Go

package resolvconf
import (
"bytes"
"io/ioutil"
"regexp"
"strings"
"sync"
log "github.com/Sirupsen/logrus"
"github.com/docker/docker/utils"
)
var (
// Note: the default IPv4 & IPv6 resolvers are set to Google's Public DNS
defaultIPv4Dns = []string{"nameserver 8.8.8.8", "nameserver 8.8.4.4"}
defaultIPv6Dns = []string{"nameserver 2001:4860:4860::8888", "nameserver 2001:4860:4860::8844"}
ipv4NumBlock = `(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)`
ipv4Address = `(` + ipv4NumBlock + `\.){3}` + ipv4NumBlock
// This is not an IPv6 address verifier as it will accept a super-set of IPv6, and also
// will *not match* IPv4-Embedded IPv6 Addresses (RFC6052), but that and other variants
// -- e.g. other link-local types -- either won't work in containers or are unnecessary.
// For readability and sufficiency for Docker purposes this seemed more reasonable than a
// 1000+ character regexp with exact and complete IPv6 validation
ipv6Address = `([0-9A-Fa-f]{0,4}:){2,7}([0-9A-Fa-f]{0,4})`
localhostRegexp = regexp.MustCompile(`(?m)^nameserver\s+((127\.([0-9]{1,3}.){2}[0-9]{1,3})|(::1))\s*\n*`)
nsIPv6Regexp = regexp.MustCompile(`(?m)^nameserver\s+` + ipv6Address + `\s*\n*`)
nsRegexp = regexp.MustCompile(`^\s*nameserver\s*((` + ipv4Address + `)|(` + ipv6Address + `))\s*$`)
searchRegexp = regexp.MustCompile(`^\s*search\s*(([^\s]+\s*)*)$`)
)
var lastModified struct {
sync.Mutex
sha256 string
contents []byte
}
func Get() ([]byte, error) {
resolv, err := ioutil.ReadFile("/etc/resolv.conf")
if err != nil {
return nil, err
}
return resolv, nil
}
// Retrieves the host /etc/resolv.conf file, checks against the last hash
// and, if modified since last check, returns the bytes and new hash.
// This feature is used by the resolv.conf updater for containers
func GetIfChanged() ([]byte, string, error) {
lastModified.Lock()
defer lastModified.Unlock()
resolv, err := ioutil.ReadFile("/etc/resolv.conf")
if err != nil {
return nil, "", err
}
newHash, err := utils.HashData(bytes.NewReader(resolv))
if err != nil {
return nil, "", err
}
if lastModified.sha256 != newHash {
lastModified.sha256 = newHash
lastModified.contents = resolv
return resolv, newHash, nil
}
// nothing changed, so return no data
return nil, "", nil
}
// retrieve the last used contents and hash of the host resolv.conf
// Used by containers updating on restart
func GetLastModified() ([]byte, string) {
lastModified.Lock()
defer lastModified.Unlock()
return lastModified.contents, lastModified.sha256
}
// FilterResolvDns has two main jobs:
// 1. It looks for localhost (127.*|::1) entries in the provided
// resolv.conf, removing local nameserver entries, and, if the resulting
// cleaned config has no defined nameservers left, adds default DNS entries
// 2. Given the caller provides the enable/disable state of IPv6, the filter
// code will remove all IPv6 nameservers if it is not enabled for containers
//
// It also returns a boolean to notify the caller if changes were made at all
func FilterResolvDns(resolvConf []byte, ipv6Enabled bool) ([]byte, bool) {
changed := false
cleanedResolvConf := localhostRegexp.ReplaceAll(resolvConf, []byte{})
// if IPv6 is not enabled, also clean out any IPv6 address nameserver
if !ipv6Enabled {
cleanedResolvConf = nsIPv6Regexp.ReplaceAll(cleanedResolvConf, []byte{})
}
// if the resulting resolvConf has no more nameservers defined, add appropriate
// default DNS servers for IPv4 and (optionally) IPv6
if len(GetNameservers(cleanedResolvConf)) == 0 {
log.Infof("No non-localhost DNS nameservers are left in resolv.conf. Using default external servers : %v", defaultIPv4Dns)
dns := defaultIPv4Dns
if ipv6Enabled {
log.Infof("IPv6 enabled; Adding default IPv6 external servers : %v", defaultIPv6Dns)
dns = append(dns, defaultIPv6Dns...)
}
cleanedResolvConf = append(cleanedResolvConf, []byte("\n"+strings.Join(dns, "\n"))...)
}
if !bytes.Equal(resolvConf, cleanedResolvConf) {
changed = true
}
return cleanedResolvConf, changed
}
// getLines parses input into lines and strips away comments.
func getLines(input []byte, commentMarker []byte) [][]byte {
lines := bytes.Split(input, []byte("\n"))
var output [][]byte
for _, currentLine := range lines {
var commentIndex = bytes.Index(currentLine, commentMarker)
if commentIndex == -1 {
output = append(output, currentLine)
} else {
output = append(output, currentLine[:commentIndex])
}
}
return output
}
// GetNameservers returns nameservers (if any) listed in /etc/resolv.conf
func GetNameservers(resolvConf []byte) []string {
nameservers := []string{}
for _, line := range getLines(resolvConf, []byte("#")) {
var ns = nsRegexp.FindSubmatch(line)
if len(ns) > 0 {
nameservers = append(nameservers, string(ns[1]))
}
}
return nameservers
}
// GetNameserversAsCIDR returns nameservers (if any) listed in
// /etc/resolv.conf as CIDR blocks (e.g., "1.2.3.4/32")
// This function's output is intended for net.ParseCIDR
func GetNameserversAsCIDR(resolvConf []byte) []string {
nameservers := []string{}
for _, nameserver := range GetNameservers(resolvConf) {
nameservers = append(nameservers, nameserver+"/32")
}
return nameservers
}
// GetSearchDomains returns search domains (if any) listed in /etc/resolv.conf
// If more than one search line is encountered, only the contents of the last
// one is returned.
func GetSearchDomains(resolvConf []byte) []string {
domains := []string{}
for _, line := range getLines(resolvConf, []byte("#")) {
match := searchRegexp.FindSubmatch(line)
if match == nil {
continue
}
domains = strings.Fields(string(match[1]))
}
return domains
}
func Build(path string, dns, dnsSearch []string) error {
content := bytes.NewBuffer(nil)
for _, dns := range dns {
if _, err := content.WriteString("nameserver " + dns + "\n"); err != nil {
return err
}
}
if len(dnsSearch) > 0 {
if searchString := strings.Join(dnsSearch, " "); strings.Trim(searchString, " ") != "." {
if _, err := content.WriteString("search " + searchString + "\n"); err != nil {
return err
}
}
}
return ioutil.WriteFile(path, content.Bytes(), 0644)
}