github.com/toplink-cn/moby@v0.0.0-20240305205811-460b4aebdf81/libnetwork/internal/resolvconf/resolvconf.go (about)

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