diff options
-rw-r--r-- | libpod/container_easyjson.go | 2 | ||||
-rw-r--r-- | libpod/container_internal.go | 104 | ||||
-rw-r--r-- | pkg/resolvconf/dns/resolvconf.go | 28 | ||||
-rw-r--r-- | pkg/resolvconf/resolvconf.go | 242 |
4 files changed, 306 insertions, 70 deletions
diff --git a/libpod/container_easyjson.go b/libpod/container_easyjson.go index 2d0481f3b..916118aec 100644 --- a/libpod/container_easyjson.go +++ b/libpod/container_easyjson.go @@ -1,3 +1,5 @@ +// +build seccomp ostree selinux varlink exclude_graphdriver_devicemapper + // Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. package libpod diff --git a/libpod/container_internal.go b/libpod/container_internal.go index c925f070b..77bba9e85 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -15,9 +15,9 @@ import ( "github.com/containers/libpod/pkg/chrootuser" "github.com/containers/libpod/pkg/hooks" "github.com/containers/libpod/pkg/hooks/exec" + "github.com/containers/libpod/pkg/resolvconf" "github.com/containers/libpod/pkg/rootless" "github.com/containers/libpod/pkg/secrets" - "github.com/containers/libpod/pkg/util" "github.com/containers/storage" "github.com/containers/storage/pkg/archive" "github.com/containers/storage/pkg/chrootarchive" @@ -1017,12 +1017,6 @@ func (c *Container) writeStringToRundir(destFile, output string) (string, error) return filepath.Join(c.state.DestinationRunDir, destFile), nil } -type resolvConf struct { - nameServers []string - searchDomains []string - options []string -} - // generateResolvConf generates a containers resolv.conf func (c *Container) generateResolvConf() (string, error) { // Determine the endpoint for resolv.conf in case it is a symlink @@ -1030,86 +1024,56 @@ func (c *Container) generateResolvConf() (string, error) { if err != nil { return "", err } - orig, err := ioutil.ReadFile(resolvPath) + + contents, err := ioutil.ReadFile(resolvPath) if err != nil { return "", errors.Wrapf(err, "unable to read %s", resolvPath) } - if len(c.config.DNSServer) == 0 && len(c.config.DNSSearch) == 0 && len(c.config.DNSOption) == 0 { - return c.writeStringToRundir("resolv.conf", fmt.Sprintf("%s", orig)) - } - - // Read and organize the hosts /etc/resolv.conf - resolv := createResolv(string(orig[:])) - // Populate the resolv struct with user's dns search domains - if len(c.config.DNSSearch) > 0 { - resolv.searchDomains = nil - // The . character means the user doesnt want any search domains in the container - if !util.StringInSlice(".", c.config.DNSSearch) { - resolv.searchDomains = append(resolv.searchDomains, c.Config().DNSSearch...) - } + // Process the file to remove localhost nameservers + // TODO: set ipv6 enable bool more sanely + resolv, err := resolvconf.FilterResolvDNS(contents, true) + if err != nil { + return "", errors.Wrapf(err, "error parsing host resolv.conf") } - // Populate the resolv struct with user's dns servers + // Make a new resolv.conf + nameservers := resolvconf.GetNameservers(resolv.Content) if len(c.config.DNSServer) > 0 { - resolv.nameServers = nil - for _, i := range c.config.DNSServer { - resolv.nameServers = append(resolv.nameServers, i.String()) + // We store DNS servers as net.IP, so need to convert to string + nameservers = []string{} + for _, server := range c.config.DNSServer { + nameservers = append(nameservers, server.String()) } } - // Populate the resolve struct with the users dns options + search := resolvconf.GetSearchDomains(resolv.Content) + if len(c.config.DNSSearch) > 0 { + search = c.config.DNSSearch + } + + options := resolvconf.GetOptions(resolv.Content) if len(c.config.DNSOption) > 0 { - resolv.options = nil - resolv.options = append(resolv.options, c.Config().DNSOption...) + options = c.config.DNSOption } - return c.writeStringToRundir("resolv.conf", resolv.ToString()) -} -// createResolv creates a resolv struct from an input string -func createResolv(input string) resolvConf { - var resolv resolvConf - for _, line := range strings.Split(input, "\n") { - if strings.HasPrefix(line, "search") { - fields := strings.Fields(line) - if len(fields) < 2 { - logrus.Debugf("invalid resolv.conf line %s", line) - continue - } - resolv.searchDomains = append(resolv.searchDomains, fields[1:]...) - } else if strings.HasPrefix(line, "nameserver") { - fields := strings.Fields(line) - if len(fields) < 2 { - logrus.Debugf("invalid resolv.conf line %s", line) - continue - } - resolv.nameServers = append(resolv.nameServers, fields[1]) - } else if strings.HasPrefix(line, "options") { - fields := strings.Fields(line) - if len(fields) < 2 { - logrus.Debugf("invalid resolv.conf line %s", line) - continue - } - resolv.options = append(resolv.options, fields[1:]...) - } + destPath := filepath.Join(c.state.RunDir, "resolv.conf") + + if err := os.Remove(destPath); err != nil && !os.IsNotExist(err) { + return "", errors.Wrapf(err, "error removing resolv.conf for container %s", c.ID()) } - return resolv -} -//ToString returns a resolv struct in the form of a resolv.conf -func (r resolvConf) ToString() string { - var result string - // Populate the output string with search domains - result += fmt.Sprintf("search %s\n", strings.Join(r.searchDomains, " ")) - // Populate the output string with name servers - for _, i := range r.nameServers { - result += fmt.Sprintf("nameserver %s\n", i) + // Build resolv.conf + if _, err = resolvconf.Build(destPath, nameservers, search, options); err != nil { + return "", errors.Wrapf(err, "error building resolv.conf for container %s") } - // Populate the output string with dns options - for _, i := range r.options { - result += fmt.Sprintf("options %s\n", i) + + // Relabel resolv.conf for the container + if err := label.Relabel(destPath, c.config.MountLabel, false); err != nil { + return "", err } - return result + + return filepath.Join(c.state.DestinationRunDir, "resolv.conf"), nil } // generateHosts creates a containers hosts file diff --git a/pkg/resolvconf/dns/resolvconf.go b/pkg/resolvconf/dns/resolvconf.go new file mode 100644 index 000000000..cb4bd1033 --- /dev/null +++ b/pkg/resolvconf/dns/resolvconf.go @@ -0,0 +1,28 @@ +// Originally from github.com/docker/libnetwork/resolvconf/dns + +package dns + +import ( + "regexp" +) + +// IPLocalhost is a regex pattern for IPv4 or IPv6 loopback range. +const IPLocalhost = `((127\.([0-9]{1,3}\.){2}[0-9]{1,3})|(::1)$)` + +// IPv4Localhost is a regex pattern for IPv4 localhost address range. +const IPv4Localhost = `(127\.([0-9]{1,3}\.){2}[0-9]{1,3})` + +var localhostIPRegexp = regexp.MustCompile(IPLocalhost) +var localhostIPv4Regexp = regexp.MustCompile(IPv4Localhost) + +// IsLocalhost returns true if ip matches the localhost IP regular expression. +// Used for determining if nameserver settings are being passed which are +// localhost addresses +func IsLocalhost(ip string) bool { + return localhostIPRegexp.MatchString(ip) +} + +// IsIPv4Localhost returns true if ip matches the IPv4 localhost regular expression. +func IsIPv4Localhost(ip string) bool { + return localhostIPv4Regexp.MatchString(ip) +} diff --git a/pkg/resolvconf/resolvconf.go b/pkg/resolvconf/resolvconf.go new file mode 100644 index 000000000..fccd60093 --- /dev/null +++ b/pkg/resolvconf/resolvconf.go @@ -0,0 +1,242 @@ +// Package resolvconf provides utility code to query and update DNS configuration in /etc/resolv.conf. +// Originally from github.com/docker/libnetwork/resolvconf. +package resolvconf + +import ( + "bytes" + "io/ioutil" + "regexp" + "strings" + "sync" + + "github.com/containers/libpod/pkg/resolvconf/dns" + "github.com/docker/docker/pkg/ioutils" + "github.com/sirupsen/logrus" +) + +const ( + // DefaultResolvConf points to the default file used for dns configuration on a linux machine + DefaultResolvConf = "/etc/resolv.conf" +) + +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})(%\w+)?` + + localhostNSRegexp = regexp.MustCompile(`(?m)^nameserver\s+` + dns.IPLocalhost + `\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*)*)$`) + optionsRegexp = regexp.MustCompile(`^\s*options\s*(([^\s]+\s*)*)$`) +) + +var lastModified struct { + sync.Mutex + sha256 string + contents []byte +} + +// File contains the resolv.conf content and its hash +type File struct { + Content []byte + Hash string +} + +// Get returns the contents of /etc/resolv.conf and its hash +func Get() (*File, error) { + return GetSpecific(DefaultResolvConf) +} + +// GetSpecific returns the contents of the user specified resolv.conf file and its hash +func GetSpecific(path string) (*File, error) { + resolv, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + hash, err := ioutils.HashData(bytes.NewReader(resolv)) + if err != nil { + return nil, err + } + return &File{Content: resolv, Hash: hash}, nil +} + +// GetIfChanged 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() (*File, error) { + lastModified.Lock() + defer lastModified.Unlock() + + resolv, err := ioutil.ReadFile("/etc/resolv.conf") + if err != nil { + return nil, err + } + newHash, err := ioutils.HashData(bytes.NewReader(resolv)) + if err != nil { + return nil, err + } + if lastModified.sha256 != newHash { + lastModified.sha256 = newHash + lastModified.contents = resolv + return &File{Content: resolv, Hash: newHash}, nil + } + // nothing changed, so return no data + return nil, nil +} + +// GetLastModified retrieves the last used contents and hash of the host resolv.conf. +// Used by containers updating on restart +func GetLastModified() *File { + lastModified.Lock() + defer lastModified.Unlock() + + return &File{Content: lastModified.contents, Hash: lastModified.sha256} +} + +// FilterResolvDNS cleans up the config in resolvConf. It 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 +// +func FilterResolvDNS(resolvConf []byte, ipv6Enabled bool) (*File, error) { + cleanedResolvConf := localhostNSRegexp.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 { + logrus.Infof("No non-localhost DNS nameservers are left in resolv.conf. Using default external servers: %v", defaultIPv4Dns) + dns := defaultIPv4Dns + if ipv6Enabled { + logrus.Infof("IPv6 enabled; Adding default IPv6 external servers: %v", defaultIPv6Dns) + dns = append(dns, defaultIPv6Dns...) + } + cleanedResolvConf = append(cleanedResolvConf, []byte("\n"+strings.Join(dns, "\n"))...) + } + hash, err := ioutils.HashData(bytes.NewReader(cleanedResolvConf)) + if err != nil { + return nil, err + } + return &File{Content: cleanedResolvConf, Hash: hash}, nil +} + +// 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("#")) { + 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) { + var address string + // If IPv6, strip zone if present + if strings.Contains(nameserver, ":") { + address = strings.Split(nameserver, "%")[0] + "/128" + } else { + address = nameserver + "/32" + } + nameservers = append(nameservers, address) + } + 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 +} + +// GetOptions returns options (if any) listed in /etc/resolv.conf +// If more than one options line is encountered, only the contents of the last +// one is returned. +func GetOptions(resolvConf []byte) []string { + options := []string{} + for _, line := range getLines(resolvConf, []byte("#")) { + match := optionsRegexp.FindSubmatch(line) + if match == nil { + continue + } + options = strings.Fields(string(match[1])) + } + return options +} + +// Build writes a configuration file to path containing a "nameserver" entry +// for every element in dns, a "search" entry for every element in +// dnsSearch, and an "options" entry for every element in dnsOptions. +func Build(path string, dns, dnsSearch, dnsOptions []string) (*File, error) { + content := bytes.NewBuffer(nil) + if len(dnsSearch) > 0 { + if searchString := strings.Join(dnsSearch, " "); strings.Trim(searchString, " ") != "." { + if _, err := content.WriteString("search " + searchString + "\n"); err != nil { + return nil, err + } + } + } + for _, dns := range dns { + if _, err := content.WriteString("nameserver " + dns + "\n"); err != nil { + return nil, err + } + } + if len(dnsOptions) > 0 { + if optsString := strings.Join(dnsOptions, " "); strings.Trim(optsString, " ") != "" { + if _, err := content.WriteString("options " + optsString + "\n"); err != nil { + return nil, err + } + } + } + + hash, err := ioutils.HashData(bytes.NewReader(content.Bytes())) + if err != nil { + return nil, err + } + + return &File{Content: content.Bytes(), Hash: hash}, ioutil.WriteFile(path, content.Bytes(), 0644) +} |