summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--libpod/container_easyjson.go2
-rw-r--r--libpod/container_internal.go104
-rw-r--r--pkg/resolvconf/dns/resolvconf.go28
-rw-r--r--pkg/resolvconf/resolvconf.go242
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)
+}