github.com/containers/libpod@v1.9.4-0.20220419124438-4284fd425507/pkg/resolvconf/resolvconf.go (about)

     1  // Package resolvconf provides utility code to query and update DNS configuration in /etc/resolv.conf.
     2  // Originally from github.com/docker/libnetwork/resolvconf.
     3  package resolvconf
     4  
     5  import (
     6  	"bytes"
     7  	"io/ioutil"
     8  	"regexp"
     9  	"strings"
    10  	"sync"
    11  
    12  	"github.com/containers/libpod/pkg/resolvconf/dns"
    13  	"github.com/containers/storage/pkg/ioutils"
    14  	"github.com/sirupsen/logrus"
    15  )
    16  
    17  const (
    18  	// DefaultResolvConf points to the default file used for dns configuration on a linux machine
    19  	DefaultResolvConf = "/etc/resolv.conf"
    20  )
    21  
    22  var (
    23  	// Note: the default IPv4 & IPv6 resolvers are set to Google's Public DNS
    24  	defaultIPv4Dns = []string{"nameserver 8.8.8.8", "nameserver 8.8.4.4"}
    25  	defaultIPv6Dns = []string{"nameserver 2001:4860:4860::8888", "nameserver 2001:4860:4860::8844"}
    26  	ipv4NumBlock   = `(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)`
    27  	ipv4Address    = `(` + ipv4NumBlock + `\.){3}` + ipv4NumBlock
    28  	// This is not an IPv6 address verifier as it will accept a super-set of IPv6, and also
    29  	// will *not match* IPv4-Embedded IPv6 Addresses (RFC6052), but that and other variants
    30  	// -- e.g. other link-local types -- either won't work in containers or are unnecessary.
    31  	// For readability and sufficiency for Docker purposes this seemed more reasonable than a
    32  	// 1000+ character regexp with exact and complete IPv6 validation
    33  	ipv6Address = `([0-9A-Fa-f]{0,4}:){2,7}([0-9A-Fa-f]{0,4})(%\w+)?`
    34  
    35  	localhostNSRegexp = regexp.MustCompile(`(?m)^nameserver\s+` + dns.IPLocalhost + `\s*\n*`)
    36  	nsIPv6Regexp      = regexp.MustCompile(`(?m)^nameserver\s+` + ipv6Address + `\s*\n*`)
    37  	nsRegexp          = regexp.MustCompile(`^\s*nameserver\s*((` + ipv4Address + `)|(` + ipv6Address + `))\s*$`)
    38  	searchRegexp      = regexp.MustCompile(`^\s*search\s*(([^\s]+\s*)*)$`)
    39  	optionsRegexp     = regexp.MustCompile(`^\s*options\s*(([^\s]+\s*)*)$`)
    40  )
    41  
    42  var lastModified struct {
    43  	sync.Mutex
    44  	sha256   string
    45  	contents []byte
    46  }
    47  
    48  // File contains the resolv.conf content and its hash
    49  type File struct {
    50  	Content []byte
    51  	Hash    string
    52  }
    53  
    54  // Get returns the contents of /etc/resolv.conf and its hash
    55  func Get() (*File, error) {
    56  	return GetSpecific(DefaultResolvConf)
    57  }
    58  
    59  // GetSpecific returns the contents of the user specified resolv.conf file and its hash
    60  func GetSpecific(path string) (*File, error) {
    61  	resolv, err := ioutil.ReadFile(path)
    62  	if err != nil {
    63  		return nil, err
    64  	}
    65  	hash, err := ioutils.HashData(bytes.NewReader(resolv))
    66  	if err != nil {
    67  		return nil, err
    68  	}
    69  	return &File{Content: resolv, Hash: hash}, nil
    70  }
    71  
    72  // GetIfChanged retrieves the host /etc/resolv.conf file, checks against the last hash
    73  // and, if modified since last check, returns the bytes and new hash.
    74  // This feature is used by the resolv.conf updater for containers
    75  func GetIfChanged() (*File, error) {
    76  	lastModified.Lock()
    77  	defer lastModified.Unlock()
    78  
    79  	resolv, err := ioutil.ReadFile("/etc/resolv.conf")
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  	newHash, err := ioutils.HashData(bytes.NewReader(resolv))
    84  	if err != nil {
    85  		return nil, err
    86  	}
    87  	if lastModified.sha256 != newHash {
    88  		lastModified.sha256 = newHash
    89  		lastModified.contents = resolv
    90  		return &File{Content: resolv, Hash: newHash}, nil
    91  	}
    92  	// nothing changed, so return no data
    93  	return nil, nil
    94  }
    95  
    96  // GetLastModified retrieves the last used contents and hash of the host resolv.conf.
    97  // Used by containers updating on restart
    98  func GetLastModified() *File {
    99  	lastModified.Lock()
   100  	defer lastModified.Unlock()
   101  
   102  	return &File{Content: lastModified.contents, Hash: lastModified.sha256}
   103  }
   104  
   105  // FilterResolvDNS cleans up the config in resolvConf.  It has two main jobs:
   106  // 1. If a netns is enabled, it looks for localhost (127.*|::1) entries in the provided
   107  //    resolv.conf, removing local nameserver entries, and, if the resulting
   108  //    cleaned config has no defined nameservers left, adds default DNS entries
   109  // 2. Given the caller provides the enable/disable state of IPv6, the filter
   110  //    code will remove all IPv6 nameservers if it is not enabled for containers
   111  //
   112  func FilterResolvDNS(resolvConf []byte, ipv6Enabled bool, netnsEnabled bool) (*File, error) {
   113  	// If we're using the host netns, we have nothing to do besides hash the file.
   114  	if !netnsEnabled {
   115  		hash, err := ioutils.HashData(bytes.NewReader(resolvConf))
   116  		if err != nil {
   117  			return nil, err
   118  		}
   119  		return &File{Content: resolvConf, Hash: hash}, nil
   120  	}
   121  	cleanedResolvConf := localhostNSRegexp.ReplaceAll(resolvConf, []byte{})
   122  	// if IPv6 is not enabled, also clean out any IPv6 address nameserver
   123  	if !ipv6Enabled {
   124  		cleanedResolvConf = nsIPv6Regexp.ReplaceAll(cleanedResolvConf, []byte{})
   125  	}
   126  	// if the resulting resolvConf has no more nameservers defined, add appropriate
   127  	// default DNS servers for IPv4 and (optionally) IPv6
   128  	if len(GetNameservers(cleanedResolvConf)) == 0 {
   129  		logrus.Infof("No non-localhost DNS nameservers are left in resolv.conf. Using default external servers: %v", defaultIPv4Dns)
   130  		dns := defaultIPv4Dns
   131  		if ipv6Enabled {
   132  			logrus.Infof("IPv6 enabled; Adding default IPv6 external servers: %v", defaultIPv6Dns)
   133  			dns = append(dns, defaultIPv6Dns...)
   134  		}
   135  		cleanedResolvConf = append(cleanedResolvConf, []byte("\n"+strings.Join(dns, "\n"))...)
   136  	}
   137  	hash, err := ioutils.HashData(bytes.NewReader(cleanedResolvConf))
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  	return &File{Content: cleanedResolvConf, Hash: hash}, nil
   142  }
   143  
   144  // getLines parses input into lines and strips away comments.
   145  func getLines(input []byte, commentMarker []byte) [][]byte {
   146  	lines := bytes.Split(input, []byte("\n"))
   147  	var output [][]byte
   148  	for _, currentLine := range lines {
   149  		var commentIndex = bytes.Index(currentLine, commentMarker)
   150  		if commentIndex == -1 {
   151  			output = append(output, currentLine)
   152  		} else {
   153  			output = append(output, currentLine[:commentIndex])
   154  		}
   155  	}
   156  	return output
   157  }
   158  
   159  // GetNameservers returns nameservers (if any) listed in /etc/resolv.conf
   160  func GetNameservers(resolvConf []byte) []string {
   161  	nameservers := []string{}
   162  	for _, line := range getLines(resolvConf, []byte("#")) {
   163  		ns := nsRegexp.FindSubmatch(line)
   164  		if len(ns) > 0 {
   165  			nameservers = append(nameservers, string(ns[1]))
   166  		}
   167  	}
   168  	return nameservers
   169  }
   170  
   171  // GetNameserversAsCIDR returns nameservers (if any) listed in
   172  // /etc/resolv.conf as CIDR blocks (e.g., "1.2.3.4/32")
   173  // This function's output is intended for net.ParseCIDR
   174  func GetNameserversAsCIDR(resolvConf []byte) []string {
   175  	nameservers := []string{}
   176  	for _, nameserver := range GetNameservers(resolvConf) {
   177  		var address string
   178  		// If IPv6, strip zone if present
   179  		if strings.Contains(nameserver, ":") {
   180  			address = strings.Split(nameserver, "%")[0] + "/128"
   181  		} else {
   182  			address = nameserver + "/32"
   183  		}
   184  		nameservers = append(nameservers, address)
   185  	}
   186  	return nameservers
   187  }
   188  
   189  // GetSearchDomains returns search domains (if any) listed in /etc/resolv.conf
   190  // If more than one search line is encountered, only the contents of the last
   191  // one is returned.
   192  func GetSearchDomains(resolvConf []byte) []string {
   193  	domains := []string{}
   194  	for _, line := range getLines(resolvConf, []byte("#")) {
   195  		match := searchRegexp.FindSubmatch(line)
   196  		if match == nil {
   197  			continue
   198  		}
   199  		domains = strings.Fields(string(match[1]))
   200  	}
   201  	return domains
   202  }
   203  
   204  // GetOptions returns options (if any) listed in /etc/resolv.conf
   205  // If more than one options line is encountered, only the contents of the last
   206  // one is returned.
   207  func GetOptions(resolvConf []byte) []string {
   208  	options := []string{}
   209  	for _, line := range getLines(resolvConf, []byte("#")) {
   210  		match := optionsRegexp.FindSubmatch(line)
   211  		if match == nil {
   212  			continue
   213  		}
   214  		options = strings.Fields(string(match[1]))
   215  	}
   216  	return options
   217  }
   218  
   219  // Build writes a configuration file to path containing a "nameserver" entry
   220  // for every element in dns, a "search" entry for every element in
   221  // dnsSearch, and an "options" entry for every element in dnsOptions.
   222  func Build(path string, dns, dnsSearch, dnsOptions []string) (*File, error) {
   223  	content := bytes.NewBuffer(nil)
   224  	if len(dnsSearch) > 0 {
   225  		if searchString := strings.Join(dnsSearch, " "); strings.Trim(searchString, " ") != "." {
   226  			if _, err := content.WriteString("search " + searchString + "\n"); err != nil {
   227  				return nil, err
   228  			}
   229  		}
   230  	}
   231  	for _, dns := range dns {
   232  		if _, err := content.WriteString("nameserver " + dns + "\n"); err != nil {
   233  			return nil, err
   234  		}
   235  	}
   236  	if len(dnsOptions) > 0 {
   237  		if optsString := strings.Join(dnsOptions, " "); strings.Trim(optsString, " ") != "" {
   238  			if _, err := content.WriteString("options " + optsString + "\n"); err != nil {
   239  				return nil, err
   240  			}
   241  		}
   242  	}
   243  
   244  	hash, err := ioutils.HashData(bytes.NewReader(content.Bytes()))
   245  	if err != nil {
   246  		return nil, err
   247  	}
   248  
   249  	return &File{Content: content.Bytes(), Hash: hash}, ioutil.WriteFile(path, content.Bytes(), 0644)
   250  }