github.com/tonistiigi/docker@v0.10.1-0.20240229224939-974013b0dc6a/libnetwork/internal/resolvconf/resolvconf.go (about)

     1  //go:build go1.21
     2  
     3  package resolvconf
     4  
     5  import (
     6  	"bufio"
     7  	"bytes"
     8  	"context"
     9  	_ "embed"
    10  	"fmt"
    11  	"io"
    12  	"io/fs"
    13  	"net/netip"
    14  	"os"
    15  	"slices"
    16  	"strconv"
    17  	"strings"
    18  	"text/template"
    19  
    20  	"github.com/containerd/log"
    21  	"github.com/docker/docker/errdefs"
    22  	"github.com/docker/docker/pkg/ioutils"
    23  	"github.com/opencontainers/go-digest"
    24  	"github.com/pkg/errors"
    25  )
    26  
    27  // Fallback nameservers, to use if none can be obtained from the host or command
    28  // line options.
    29  var (
    30  	defaultIPv4NSs = []netip.Addr{
    31  		netip.MustParseAddr("8.8.8.8"),
    32  		netip.MustParseAddr("8.8.4.4"),
    33  	}
    34  	defaultIPv6NSs = []netip.Addr{
    35  		netip.MustParseAddr("2001:4860:4860::8888"),
    36  		netip.MustParseAddr("2001:4860:4860::8844"),
    37  	}
    38  )
    39  
    40  // ResolvConf represents a resolv.conf file. It can be constructed by
    41  // reading a resolv.conf file, using method Parse().
    42  type ResolvConf struct {
    43  	nameServers []netip.Addr
    44  	search      []string
    45  	options     []string
    46  	other       []string // Unrecognised directives from the host's file, if any.
    47  
    48  	md metadata
    49  }
    50  
    51  // ExtDNSEntry represents a nameserver address that was removed from the
    52  // container's resolv.conf when it was transformed by TransformForIntNS(). These
    53  // are addresses read from the host's file, or applied via an override ('--dns').
    54  type ExtDNSEntry struct {
    55  	Addr         netip.Addr
    56  	HostLoopback bool // The address is loopback, in the host's namespace.
    57  }
    58  
    59  func (ed ExtDNSEntry) String() string {
    60  	if ed.HostLoopback {
    61  		return fmt.Sprintf("host(%s)", ed.Addr)
    62  	}
    63  	return ed.Addr.String()
    64  }
    65  
    66  // metadata is used to track where components of the generated file have come
    67  // from, in order to generate comments in the file for debug/info. Struct members
    68  // are exported for use by 'text/template'.
    69  type metadata struct {
    70  	SourcePath      string
    71  	Header          string
    72  	NSOverride      bool
    73  	SearchOverride  bool
    74  	OptionsOverride bool
    75  	NDotsFrom       string
    76  	UsedDefaultNS   bool
    77  	Transform       string
    78  	InvalidNSs      []string
    79  	ExtNameServers  []ExtDNSEntry
    80  }
    81  
    82  // Load opens a file at path and parses it as a resolv.conf file.
    83  // On error, the returned ResolvConf will be zero-valued.
    84  func Load(path string) (ResolvConf, error) {
    85  	f, err := os.Open(path)
    86  	if err != nil {
    87  		return ResolvConf{}, err
    88  	}
    89  	defer f.Close()
    90  	return Parse(f, path)
    91  }
    92  
    93  // Parse parses a resolv.conf file from reader.
    94  // path is optional if reader is an *os.File.
    95  // On error, the returned ResolvConf will be zero-valued.
    96  func Parse(reader io.Reader, path string) (ResolvConf, error) {
    97  	var rc ResolvConf
    98  	rc.md.SourcePath = path
    99  	if path == "" {
   100  		if namer, ok := reader.(interface{ Name() string }); ok {
   101  			rc.md.SourcePath = namer.Name()
   102  		}
   103  	}
   104  
   105  	scanner := bufio.NewScanner(reader)
   106  	for scanner.Scan() {
   107  		rc.processLine(scanner.Text())
   108  	}
   109  	if err := scanner.Err(); err != nil {
   110  		return ResolvConf{}, errdefs.System(err)
   111  	}
   112  	if _, ok := rc.Option("ndots"); ok {
   113  		rc.md.NDotsFrom = "host"
   114  	}
   115  	return rc, nil
   116  }
   117  
   118  // SetHeader sets the content to be included verbatim at the top of the
   119  // generated resolv.conf file. No formatting or checking is done on the
   120  // string. It must be valid resolv.conf syntax. (Comments must have '#'
   121  // or ';' in the first column of each line).
   122  //
   123  // For example:
   124  //
   125  //	SetHeader("# My resolv.conf\n# This file was generated.")
   126  func (rc *ResolvConf) SetHeader(c string) {
   127  	rc.md.Header = c
   128  }
   129  
   130  // NameServers returns addresses used in nameserver directives.
   131  func (rc *ResolvConf) NameServers() []netip.Addr {
   132  	return slices.Clone(rc.nameServers)
   133  }
   134  
   135  // OverrideNameServers replaces the current set of nameservers.
   136  func (rc *ResolvConf) OverrideNameServers(nameServers []netip.Addr) {
   137  	rc.nameServers = nameServers
   138  	rc.md.NSOverride = true
   139  }
   140  
   141  // Search returns the current DNS search domains.
   142  func (rc *ResolvConf) Search() []string {
   143  	return slices.Clone(rc.search)
   144  }
   145  
   146  // OverrideSearch replaces the current DNS search domains.
   147  func (rc *ResolvConf) OverrideSearch(search []string) {
   148  	var filtered []string
   149  	for _, s := range search {
   150  		if s != "." {
   151  			filtered = append(filtered, s)
   152  		}
   153  	}
   154  	rc.search = filtered
   155  	rc.md.SearchOverride = true
   156  }
   157  
   158  // Options returns the current options.
   159  func (rc *ResolvConf) Options() []string {
   160  	return slices.Clone(rc.options)
   161  }
   162  
   163  // Option finds the last option named search, and returns (value, true) if
   164  // found, else ("", false). Options are treated as "name:value", where the
   165  // ":value" may be omitted.
   166  //
   167  // For example, for "ndots:1 edns0":
   168  //
   169  //	Option("ndots") -> ("1", true)
   170  //	Option("edns0") -> ("", true)
   171  func (rc *ResolvConf) Option(search string) (string, bool) {
   172  	for i := len(rc.options) - 1; i >= 0; i -= 1 {
   173  		k, v, _ := strings.Cut(rc.options[i], ":")
   174  		if k == search {
   175  			return v, true
   176  		}
   177  	}
   178  	return "", false
   179  }
   180  
   181  // OverrideOptions replaces the current DNS options.
   182  func (rc *ResolvConf) OverrideOptions(options []string) {
   183  	rc.options = slices.Clone(options)
   184  	rc.md.NDotsFrom = ""
   185  	if _, exists := rc.Option("ndots"); exists {
   186  		rc.md.NDotsFrom = "override"
   187  	}
   188  	rc.md.OptionsOverride = true
   189  }
   190  
   191  // AddOption adds a single DNS option.
   192  func (rc *ResolvConf) AddOption(option string) {
   193  	if len(option) > 6 && option[:6] == "ndots:" {
   194  		rc.md.NDotsFrom = "internal"
   195  	}
   196  	rc.options = append(rc.options, option)
   197  }
   198  
   199  // TransformForLegacyNw makes sure the resolv.conf file will be suitable for
   200  // use in a legacy network (one that has no internal resolver).
   201  //   - Remove loopback addresses inherited from the host's resolv.conf, because
   202  //     they'll only work in the host's namespace.
   203  //   - Remove IPv6 addresses if !ipv6.
   204  //   - Add default nameservers if there are no addresses left.
   205  func (rc *ResolvConf) TransformForLegacyNw(ipv6 bool) {
   206  	rc.md.Transform = "legacy"
   207  	if rc.md.NSOverride {
   208  		return
   209  	}
   210  	var filtered []netip.Addr
   211  	for _, addr := range rc.nameServers {
   212  		if !addr.IsLoopback() && (!addr.Is6() || ipv6) {
   213  			filtered = append(filtered, addr)
   214  		}
   215  	}
   216  	rc.nameServers = filtered
   217  	if len(rc.nameServers) == 0 {
   218  		log.G(context.TODO()).Info("No non-localhost DNS nameservers are left in resolv.conf. Using default external servers")
   219  		rc.nameServers = defaultNSAddrs(ipv6)
   220  		rc.md.UsedDefaultNS = true
   221  	}
   222  }
   223  
   224  // TransformForIntNS makes sure the resolv.conf file will be suitable for
   225  // use in a network sandbox that has an internal DNS resolver.
   226  //   - Add internalNS as a nameserver.
   227  //   - Remove other nameservers, stashing them as ExtNameServers for the
   228  //     internal resolver to use. (Apart from IPv6 nameservers, if keepIPv6.)
   229  //   - Mark ExtNameServers that must be used in the host namespace.
   230  //   - If no ExtNameServer addresses are found, use the defaults.
   231  //   - Return an error if an "ndots" option inherited from the host's config, or
   232  //     supplied in an override is not valid.
   233  //   - Ensure there's an 'options' value for each entry in reqdOptions. If the
   234  //     option includes a ':', and an option with a matching prefix exists, it
   235  //     is not modified.
   236  func (rc *ResolvConf) TransformForIntNS(
   237  	keepIPv6 bool,
   238  	internalNS netip.Addr,
   239  	reqdOptions []string,
   240  ) ([]ExtDNSEntry, error) {
   241  	// The transformed config must list the internal nameserver.
   242  	newNSs := []netip.Addr{internalNS}
   243  	// Filter out other nameservers, keeping them for use as upstream nameservers by the
   244  	// internal nameserver.
   245  	rc.md.ExtNameServers = nil
   246  	for _, addr := range rc.nameServers {
   247  		// The internal resolver only uses IPv4 addresses so, keep IPv6 nameservers in
   248  		// the container's file if keepIPv6, else drop them.
   249  		if addr.Is6() {
   250  			if keepIPv6 {
   251  				newNSs = append(newNSs, addr)
   252  			}
   253  		} else {
   254  			// Extract this NS. Mark loopback addresses that did not come from an override as
   255  			// 'HostLoopback'. Upstream requests for these servers will be made in the host's
   256  			// network namespace. (So, '--dns 127.0.0.53' means use a nameserver listening on
   257  			// the container's loopback interface. But, if the host's resolv.conf contains
   258  			// 'nameserver 127.0.0.53', the host's resolver will be used.)
   259  			//
   260  			//  TODO(robmry) - why only loopback addresses?
   261  			//   Addresses from the host's resolv.conf must be usable in the host's namespace,
   262  			//   and a lookup from the container's namespace is more expensive? And, for
   263  			//   example, if the host has a nameserver with an IPv6 LL address with a zone-id,
   264  			//   it won't work from the container's namespace (now, while the address is left in
   265  			//   the container's resolv.conf, or in future for the internal resolver).
   266  			rc.md.ExtNameServers = append(rc.md.ExtNameServers, ExtDNSEntry{
   267  				Addr:         addr,
   268  				HostLoopback: addr.IsLoopback() && !rc.md.NSOverride,
   269  			})
   270  		}
   271  	}
   272  	rc.nameServers = newNSs
   273  
   274  	// If there are no external nameservers, and the only nameserver left is the
   275  	// internal resolver, use the defaults as ext nameservers.
   276  	if len(rc.md.ExtNameServers) == 0 && len(rc.nameServers) == 1 {
   277  		log.G(context.TODO()).Info("No non-localhost DNS nameservers are left in resolv.conf. Using default external servers")
   278  		for _, addr := range defaultNSAddrs(keepIPv6) {
   279  			rc.md.ExtNameServers = append(rc.md.ExtNameServers, ExtDNSEntry{Addr: addr})
   280  		}
   281  		rc.md.UsedDefaultNS = true
   282  	}
   283  
   284  	// Validate the ndots option from host config or overrides, if present.
   285  	// TODO(robmry) - pre-existing behaviour, but ...
   286  	//   Validating ndots from an override is good, but not-liking something in the
   287  	//   host's resolv.conf isn't a reason to fail - just remove? (And it'll be
   288  	//   replaced by the value in reqdOptions, if given.)
   289  	if ndots, exists := rc.Option("ndots"); exists {
   290  		if n, err := strconv.Atoi(ndots); err != nil || n < 0 {
   291  			return nil, errdefs.InvalidParameter(
   292  				fmt.Errorf("invalid number for ndots option: %v", ndots))
   293  		}
   294  	}
   295  	// For each option required by the nameserver, add it if not already
   296  	// present (if the option already has a value don't change it).
   297  	for _, opt := range reqdOptions {
   298  		optName, _, _ := strings.Cut(opt, ":")
   299  		if _, exists := rc.Option(optName); !exists {
   300  			rc.AddOption(opt)
   301  		}
   302  	}
   303  
   304  	rc.md.Transform = "internal resolver"
   305  	return slices.Clone(rc.md.ExtNameServers), nil
   306  }
   307  
   308  // Generate returns content suitable for writing to a resolv.conf file. If comments
   309  // is true, the file will include header information if supplied, and a trailing
   310  // comment that describes how the file was constructed and lists external resolvers.
   311  func (rc *ResolvConf) Generate(comments bool) ([]byte, error) {
   312  	s := struct {
   313  		Md          *metadata
   314  		NameServers []netip.Addr
   315  		Search      []string
   316  		Options     []string
   317  		Other       []string
   318  		Overrides   []string
   319  		Comments    bool
   320  	}{
   321  		Md:          &rc.md,
   322  		NameServers: rc.nameServers,
   323  		Search:      rc.search,
   324  		Options:     rc.options,
   325  		Other:       rc.other,
   326  		Comments:    comments,
   327  	}
   328  	if rc.md.NSOverride {
   329  		s.Overrides = append(s.Overrides, "nameservers")
   330  	}
   331  	if rc.md.SearchOverride {
   332  		s.Overrides = append(s.Overrides, "search")
   333  	}
   334  	if rc.md.OptionsOverride {
   335  		s.Overrides = append(s.Overrides, "options")
   336  	}
   337  
   338  	const templateText = `{{if .Comments}}{{with .Md.Header}}{{.}}
   339  
   340  {{end}}{{end}}{{range .NameServers -}}
   341  nameserver {{.}}
   342  {{end}}{{with .Search -}}
   343  search {{join . " "}}
   344  {{end}}{{with .Options -}}
   345  options {{join . " "}}
   346  {{end}}{{with .Other -}}
   347  {{join . "\n"}}
   348  {{end}}{{if .Comments}}
   349  # Based on host file: '{{.Md.SourcePath}}'{{with .Md.Transform}} ({{.}}){{end}}
   350  {{if .Md.UsedDefaultNS -}}
   351  # Used default nameservers.
   352  {{end -}}
   353  {{with .Md.ExtNameServers -}}
   354  # ExtServers: {{.}}
   355  {{end -}}
   356  {{with .Md.InvalidNSs -}}
   357  # Invalid nameservers: {{.}}
   358  {{end -}}
   359  # Overrides: {{.Overrides}}
   360  {{with .Md.NDotsFrom -}}
   361  # Option ndots from: {{.}}
   362  {{end -}}
   363  {{end -}}
   364  `
   365  
   366  	funcs := template.FuncMap{"join": strings.Join}
   367  	var buf bytes.Buffer
   368  	templ, err := template.New("summary").Funcs(funcs).Parse(templateText)
   369  	if err != nil {
   370  		return nil, errdefs.System(err)
   371  	}
   372  	if err := templ.Execute(&buf, s); err != nil {
   373  		return nil, errdefs.System(err)
   374  	}
   375  	return buf.Bytes(), nil
   376  }
   377  
   378  // WriteFile generates content and writes it to path. If hashPath is non-zero, it
   379  // also writes a file containing a hash of the content, to enable UserModified()
   380  // to determine whether the file has been modified.
   381  func (rc *ResolvConf) WriteFile(path, hashPath string, perm os.FileMode) error {
   382  	content, err := rc.Generate(true)
   383  	if err != nil {
   384  		return err
   385  	}
   386  
   387  	// Write the resolv.conf file - it's bind-mounted into the container, so can't
   388  	// move a temp file into place, just have to truncate and write it.
   389  	if err := os.WriteFile(path, content, perm); err != nil {
   390  		return errdefs.System(err)
   391  	}
   392  
   393  	// Write the hash file.
   394  	if hashPath != "" {
   395  		hashFile, err := ioutils.NewAtomicFileWriter(hashPath, perm)
   396  		if err != nil {
   397  			return errdefs.System(err)
   398  		}
   399  		defer hashFile.Close()
   400  
   401  		digest := digest.FromBytes(content)
   402  		if _, err = hashFile.Write([]byte(digest)); err != nil {
   403  			return err
   404  		}
   405  	}
   406  
   407  	return nil
   408  }
   409  
   410  // UserModified can be used to determine whether the resolv.conf file has been
   411  // modified since it was generated. It returns false with no error if the file
   412  // matches the hash, true with no error if the file no longer matches the hash,
   413  // and false with an error if the result cannot be determined.
   414  func UserModified(rcPath, rcHashPath string) (bool, error) {
   415  	currRCHash, err := os.ReadFile(rcHashPath)
   416  	if err != nil {
   417  		// If the hash file doesn't exist, can only assume it hasn't been written
   418  		// yet (so, the user hasn't modified the file it hashes).
   419  		if errors.Is(err, fs.ErrNotExist) {
   420  			return false, nil
   421  		}
   422  		return false, errors.Wrapf(err, "failed to read hash file %s", rcHashPath)
   423  	}
   424  	expected, err := digest.Parse(string(currRCHash))
   425  	if err != nil {
   426  		return false, errors.Wrapf(err, "failed to parse hash file %s", rcHashPath)
   427  	}
   428  	v := expected.Verifier()
   429  	currRC, err := os.Open(rcPath)
   430  	if err != nil {
   431  		return false, errors.Wrapf(err, "failed to open %s to check for modifications", rcPath)
   432  	}
   433  	defer currRC.Close()
   434  	if _, err := io.Copy(v, currRC); err != nil {
   435  		return false, errors.Wrapf(err, "failed to hash %s to check for modifications", rcPath)
   436  	}
   437  	return !v.Verified(), nil
   438  }
   439  
   440  func (rc *ResolvConf) processLine(line string) {
   441  	fields := strings.Fields(line)
   442  
   443  	// Strip comments.
   444  	// TODO(robmry) - ignore comment chars except in column 0.
   445  	//   This preserves old behaviour, but it's wrong. For example, resolvers
   446  	//   will honour the option in line "options # ndots:0" (and ignore the
   447  	//   "#" as an unknown option).
   448  	for i, s := range fields {
   449  		if s[0] == '#' || s[0] == ';' {
   450  			fields = fields[:i]
   451  			break
   452  		}
   453  	}
   454  	if len(fields) == 0 {
   455  		return
   456  	}
   457  
   458  	switch fields[0] {
   459  	case "nameserver":
   460  		if len(fields) < 2 {
   461  			return
   462  		}
   463  		if addr, err := netip.ParseAddr(fields[1]); err != nil {
   464  			rc.md.InvalidNSs = append(rc.md.InvalidNSs, fields[1])
   465  		} else {
   466  			rc.nameServers = append(rc.nameServers, addr)
   467  		}
   468  	case "domain":
   469  		// 'domain' is an obsolete name for 'search'.
   470  		fallthrough
   471  	case "search":
   472  		if len(fields) < 2 {
   473  			return
   474  		}
   475  		// Only the last 'search' directive is used.
   476  		rc.search = fields[1:]
   477  	case "options":
   478  		if len(fields) < 2 {
   479  			return
   480  		}
   481  		// Replace options from earlier directives.
   482  		// TODO(robmry) - preserving incorrect behaviour, options should accumulate.
   483  		//     rc.options = append(rc.options, fields[1:]...)
   484  		rc.options = fields[1:]
   485  	default:
   486  		// Copy anything that's not a recognised directive.
   487  		rc.other = append(rc.other, line)
   488  	}
   489  }
   490  
   491  func defaultNSAddrs(ipv6 bool) []netip.Addr {
   492  	var addrs []netip.Addr
   493  	addrs = append(addrs, defaultIPv4NSs...)
   494  	if ipv6 {
   495  		addrs = append(addrs, defaultIPv6NSs...)
   496  	}
   497  	return addrs
   498  }