github.com/moby/docker@v26.1.3+incompatible/libnetwork/internal/resolvconf/resolvconf.go (about)

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