github.com/Prakhar-Agarwal-byte/moby@v0.0.0-20231027092010-a14e3e8ab87e/libnetwork/resolvconf/resolvconf.go (about)

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