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

     1  //go:build !windows
     2  
     3  package libnetwork
     4  
     5  import (
     6  	"context"
     7  	"io/fs"
     8  	"net/netip"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  
    13  	"github.com/containerd/log"
    14  	"github.com/docker/docker/errdefs"
    15  	"github.com/docker/docker/libnetwork/etchosts"
    16  	"github.com/docker/docker/libnetwork/internal/resolvconf"
    17  	"github.com/docker/docker/libnetwork/types"
    18  	"github.com/pkg/errors"
    19  )
    20  
    21  const (
    22  	defaultPrefix = "/var/lib/docker/network/files"
    23  	dirPerm       = 0o755
    24  	filePerm      = 0o644
    25  
    26  	resolverIPSandbox = "127.0.0.11"
    27  )
    28  
    29  // finishInitDNS is to be called after the container namespace has been created,
    30  // before it the user process is started. The container's support for IPv6 can be
    31  // determined at this point.
    32  func (sb *Sandbox) finishInitDNS() error {
    33  	if err := sb.buildHostsFile(); err != nil {
    34  		return errdefs.System(err)
    35  	}
    36  	for _, ep := range sb.Endpoints() {
    37  		if err := sb.updateHostsFile(ep.getEtcHostsAddrs()); err != nil {
    38  			return errdefs.System(err)
    39  		}
    40  	}
    41  	return nil
    42  }
    43  
    44  func (sb *Sandbox) startResolver(restore bool) {
    45  	sb.resolverOnce.Do(func() {
    46  		var err error
    47  		// The embedded resolver is always started with proxyDNS set as true, even when the sandbox is only attached to
    48  		// an internal network. This way, it's the driver responsibility to make sure `connect` syscall fails fast when
    49  		// no external connectivity is available (eg. by not setting a default gateway).
    50  		sb.resolver = NewResolver(resolverIPSandbox, true, sb)
    51  		defer func() {
    52  			if err != nil {
    53  				sb.resolver = nil
    54  			}
    55  		}()
    56  
    57  		// In the case of live restore container is already running with
    58  		// right resolv.conf contents created before. Just update the
    59  		// external DNS servers from the restored sandbox for embedded
    60  		// server to use.
    61  		if !restore {
    62  			err = sb.rebuildDNS()
    63  			if err != nil {
    64  				log.G(context.TODO()).Errorf("Updating resolv.conf failed for container %s, %q", sb.ContainerID(), err)
    65  				return
    66  			}
    67  		}
    68  		sb.resolver.SetExtServers(sb.extDNS)
    69  
    70  		if err = sb.osSbox.InvokeFunc(sb.resolver.SetupFunc(0)); err != nil {
    71  			log.G(context.TODO()).Errorf("Resolver Setup function failed for container %s, %q", sb.ContainerID(), err)
    72  			return
    73  		}
    74  
    75  		if err = sb.resolver.Start(); err != nil {
    76  			log.G(context.TODO()).Errorf("Resolver Start failed for container %s, %q", sb.ContainerID(), err)
    77  		}
    78  	})
    79  }
    80  
    81  func (sb *Sandbox) setupResolutionFiles() error {
    82  	// Create a hosts file that can be mounted during container setup. For most
    83  	// networking modes (not host networking) it will be re-created before the
    84  	// container start, once its support for IPv6 is known.
    85  	if sb.config.hostsPath == "" {
    86  		sb.config.hostsPath = defaultPrefix + "/" + sb.id + "/hosts"
    87  	}
    88  	dir, _ := filepath.Split(sb.config.hostsPath)
    89  	if err := createBasePath(dir); err != nil {
    90  		return err
    91  	}
    92  	if err := sb.buildHostsFile(); err != nil {
    93  		return err
    94  	}
    95  
    96  	return sb.setupDNS()
    97  }
    98  
    99  func (sb *Sandbox) buildHostsFile() error {
   100  	sb.restoreHostsPath()
   101  
   102  	dir, _ := filepath.Split(sb.config.hostsPath)
   103  	if err := createBasePath(dir); err != nil {
   104  		return err
   105  	}
   106  
   107  	// This is for the host mode networking
   108  	if sb.config.useDefaultSandBox && len(sb.config.extraHosts) == 0 {
   109  		// We are working under the assumption that the origin file option had been properly expressed by the upper layer
   110  		// if not here we are going to error out
   111  		if err := copyFile(sb.config.originHostsPath, sb.config.hostsPath); err != nil && !os.IsNotExist(err) {
   112  			return types.InternalErrorf("could not copy source hosts file %s to %s: %v", sb.config.originHostsPath, sb.config.hostsPath, err)
   113  		}
   114  		return nil
   115  	}
   116  
   117  	extraContent := make([]etchosts.Record, 0, len(sb.config.extraHosts))
   118  	for _, extraHost := range sb.config.extraHosts {
   119  		extraContent = append(extraContent, etchosts.Record{Hosts: extraHost.name, IP: extraHost.IP})
   120  	}
   121  
   122  	// Assume IPv6 support, unless it's definitely disabled.
   123  	buildf := etchosts.Build
   124  	if en, ok := sb.ipv6Enabled(); ok && !en {
   125  		buildf = etchosts.BuildNoIPv6
   126  	}
   127  	if err := buildf(sb.config.hostsPath, extraContent); err != nil {
   128  		return err
   129  	}
   130  
   131  	return sb.updateParentHosts()
   132  }
   133  
   134  func (sb *Sandbox) updateHostsFile(ifaceIPs []string) error {
   135  	if len(ifaceIPs) == 0 {
   136  		return nil
   137  	}
   138  
   139  	if sb.config.originHostsPath != "" {
   140  		return nil
   141  	}
   142  
   143  	// User might have provided a FQDN in hostname or split it across hostname
   144  	// and domainname.  We want the FQDN and the bare hostname.
   145  	fqdn := sb.config.hostName
   146  	if sb.config.domainName != "" {
   147  		fqdn += "." + sb.config.domainName
   148  	}
   149  	hosts := fqdn
   150  
   151  	if hostName, _, ok := strings.Cut(fqdn, "."); ok {
   152  		hosts += " " + hostName
   153  	}
   154  
   155  	var extraContent []etchosts.Record
   156  	for _, ip := range ifaceIPs {
   157  		extraContent = append(extraContent, etchosts.Record{Hosts: hosts, IP: ip})
   158  	}
   159  
   160  	sb.addHostsEntries(extraContent)
   161  	return nil
   162  }
   163  
   164  func (sb *Sandbox) addHostsEntries(recs []etchosts.Record) {
   165  	// Assume IPv6 support, unless it's definitely disabled.
   166  	if en, ok := sb.ipv6Enabled(); ok && !en {
   167  		var filtered []etchosts.Record
   168  		for _, rec := range recs {
   169  			if addr, err := netip.ParseAddr(rec.IP); err == nil && !addr.Is6() {
   170  				filtered = append(filtered, rec)
   171  			}
   172  		}
   173  		recs = filtered
   174  	}
   175  	if err := etchosts.Add(sb.config.hostsPath, recs); err != nil {
   176  		log.G(context.TODO()).Warnf("Failed adding service host entries to the running container: %v", err)
   177  	}
   178  }
   179  
   180  func (sb *Sandbox) deleteHostsEntries(recs []etchosts.Record) {
   181  	if err := etchosts.Delete(sb.config.hostsPath, recs); err != nil {
   182  		log.G(context.TODO()).Warnf("Failed deleting service host entries to the running container: %v", err)
   183  	}
   184  }
   185  
   186  func (sb *Sandbox) updateParentHosts() error {
   187  	var pSb *Sandbox
   188  
   189  	for _, update := range sb.config.parentUpdates {
   190  		// TODO(thaJeztah): was it intentional for this loop to re-use prior results of pSB? If not, we should make pSb local and always replace here.
   191  		if s, _ := sb.controller.GetSandbox(update.cid); s != nil {
   192  			pSb = s
   193  		}
   194  		if pSb == nil {
   195  			continue
   196  		}
   197  		// TODO(robmry) - filter out IPv6 addresses here if !sb.ipv6Enabled() but...
   198  		// - this is part of the implementation of '--link', which will be removed along
   199  		//   with the rest of legacy networking.
   200  		// - IPv6 addresses shouldn't be allocated if IPv6 is not available in a container,
   201  		//   and that change will come along later.
   202  		// - I think this may be dead code, it's not possible to start a parent container with
   203  		//   '--link child' unless the child has already started ("Error response from daemon:
   204  		//   Cannot link to a non running container"). So, when the child starts and this method
   205  		//   is called with updates for parents, the parents aren't running and GetSandbox()
   206  		//   returns nil.)
   207  		if err := etchosts.Update(pSb.config.hostsPath, update.ip, update.name); err != nil {
   208  			return err
   209  		}
   210  	}
   211  
   212  	return nil
   213  }
   214  
   215  func (sb *Sandbox) restoreResolvConfPath() {
   216  	if sb.config.resolvConfPath == "" {
   217  		sb.config.resolvConfPath = defaultPrefix + "/" + sb.id + "/resolv.conf"
   218  	}
   219  	sb.config.resolvConfHashFile = sb.config.resolvConfPath + ".hash"
   220  }
   221  
   222  func (sb *Sandbox) restoreHostsPath() {
   223  	if sb.config.hostsPath == "" {
   224  		sb.config.hostsPath = defaultPrefix + "/" + sb.id + "/hosts"
   225  	}
   226  }
   227  
   228  func (sb *Sandbox) setExternalResolvers(entries []resolvconf.ExtDNSEntry) {
   229  	sb.extDNS = make([]extDNSEntry, 0, len(entries))
   230  	for _, entry := range entries {
   231  		sb.extDNS = append(sb.extDNS, extDNSEntry{
   232  			IPStr:        entry.Addr.String(),
   233  			HostLoopback: entry.HostLoopback,
   234  		})
   235  	}
   236  }
   237  
   238  func (c *containerConfig) getOriginResolvConfPath() string {
   239  	if c.originResolvConfPath != "" {
   240  		return c.originResolvConfPath
   241  	}
   242  	// Fallback if not specified.
   243  	return resolvconf.Path()
   244  }
   245  
   246  // loadResolvConf reads the resolv.conf file at path, and merges in overrides for
   247  // nameservers, options, and search domains.
   248  func (sb *Sandbox) loadResolvConf(path string) (*resolvconf.ResolvConf, error) {
   249  	rc, err := resolvconf.Load(path)
   250  	if err != nil && !errors.Is(err, fs.ErrNotExist) {
   251  		return nil, err
   252  	}
   253  	// Proceed with rc, which might be zero-valued if path does not exist.
   254  
   255  	rc.SetHeader(`# Generated by Docker Engine.
   256  # This file can be edited; Docker Engine will not make further changes once it
   257  # has been modified.`)
   258  	if len(sb.config.dnsList) > 0 {
   259  		var dnsAddrs []netip.Addr
   260  		for _, ns := range sb.config.dnsList {
   261  			addr, err := netip.ParseAddr(ns)
   262  			if err != nil {
   263  				return nil, errors.Wrapf(err, "bad nameserver address %s", ns)
   264  			}
   265  			dnsAddrs = append(dnsAddrs, addr)
   266  		}
   267  		rc.OverrideNameServers(dnsAddrs)
   268  	}
   269  	if len(sb.config.dnsSearchList) > 0 {
   270  		rc.OverrideSearch(sb.config.dnsSearchList)
   271  	}
   272  	if len(sb.config.dnsOptionsList) > 0 {
   273  		rc.OverrideOptions(sb.config.dnsOptionsList)
   274  	}
   275  	return &rc, nil
   276  }
   277  
   278  // For a new sandbox, write an initial version of the container's resolv.conf. It'll
   279  // be a copy of the host's file, with overrides for nameservers, options and search
   280  // domains applied.
   281  func (sb *Sandbox) setupDNS() error {
   282  	// Make sure the directory exists.
   283  	sb.restoreResolvConfPath()
   284  	dir, _ := filepath.Split(sb.config.resolvConfPath)
   285  	if err := createBasePath(dir); err != nil {
   286  		return err
   287  	}
   288  
   289  	rc, err := sb.loadResolvConf(sb.config.getOriginResolvConfPath())
   290  	if err != nil {
   291  		return err
   292  	}
   293  	return rc.WriteFile(sb.config.resolvConfPath, sb.config.resolvConfHashFile, filePerm)
   294  }
   295  
   296  // Called when an endpoint has joined the sandbox.
   297  func (sb *Sandbox) updateDNS(ipv6Enabled bool) error {
   298  	if mod, err := resolvconf.UserModified(sb.config.resolvConfPath, sb.config.resolvConfHashFile); err != nil || mod {
   299  		return err
   300  	}
   301  
   302  	// Load the host's resolv.conf as a starting point.
   303  	rc, err := sb.loadResolvConf(sb.config.getOriginResolvConfPath())
   304  	if err != nil {
   305  		return err
   306  	}
   307  	// For host-networking, no further change is needed.
   308  	if !sb.config.useDefaultSandBox {
   309  		// The legacy bridge network has no internal nameserver. So, strip localhost
   310  		// nameservers from the host's config, then add default nameservers if there
   311  		// are none remaining.
   312  		rc.TransformForLegacyNw(ipv6Enabled)
   313  	}
   314  	return rc.WriteFile(sb.config.resolvConfPath, sb.config.resolvConfHashFile, filePerm)
   315  }
   316  
   317  // Embedded DNS server has to be enabled for this sandbox. Rebuild the container's resolv.conf.
   318  func (sb *Sandbox) rebuildDNS() error {
   319  	// Don't touch the file if the user has modified it.
   320  	if mod, err := resolvconf.UserModified(sb.config.resolvConfPath, sb.config.resolvConfHashFile); err != nil || mod {
   321  		return err
   322  	}
   323  
   324  	// Load the host's resolv.conf as a starting point.
   325  	rc, err := sb.loadResolvConf(sb.config.getOriginResolvConfPath())
   326  	if err != nil {
   327  		return err
   328  	}
   329  
   330  	// Check for IPv6 endpoints in this sandbox. If there are any, IPv6 nameservers
   331  	// will be left in the container's 'resolv.conf'.
   332  	// TODO(robmry) - preserving old behaviour, but ...
   333  	//   IPv6 nameservers should be treated like IPv4 ones, and used as upstream
   334  	//   servers for the internal resolver (if it has IPv6 connectivity). This
   335  	//   doesn't need to depend on whether there are currently any IPv6 endpoints.
   336  	//   Removing IPv6 nameservers from the container's resolv.conf will avoid the
   337  	//   problem that musl-libc's resolver tries all nameservers in parallel, so an
   338  	//   external IPv6 resolver can return NXDOMAIN before the internal resolver
   339  	//   returns the address of a container.
   340  	ipv6 := false
   341  	for _, ep := range sb.endpoints {
   342  		if ep.network.enableIPv6 {
   343  			ipv6 = true
   344  			break
   345  		}
   346  	}
   347  
   348  	intNS, err := netip.ParseAddr(sb.resolver.NameServer())
   349  	if err != nil {
   350  		return err
   351  	}
   352  
   353  	// Work out whether ndots has been set from host config or overrides.
   354  	_, sb.ndotsSet = rc.Option("ndots")
   355  	// Swap nameservers for the internal one, and make sure the required options are set.
   356  	var extNameServers []resolvconf.ExtDNSEntry
   357  	extNameServers, err = rc.TransformForIntNS(ipv6, intNS, sb.resolver.ResolverOptions())
   358  	if err != nil {
   359  		return err
   360  	}
   361  	// Extract the list of nameservers that just got swapped out, and store them as
   362  	// upstream nameservers.
   363  	sb.setExternalResolvers(extNameServers)
   364  
   365  	// Write the file for the container - preserving old behaviour, not updating the
   366  	// hash file (so, no further updates will be made).
   367  	// TODO(robmry) - I think that's probably accidental, I can't find a reason for it,
   368  	//  and the old resolvconf.Build() function wrote the file but not the hash, which
   369  	//  is surprising. But, before fixing it, a guard/flag needs to be added to
   370  	//  sb.updateDNS() to make sure that when an endpoint joins a sandbox that already
   371  	//  has an internal resolver, the container's resolv.conf is still (re)configured
   372  	//  for an internal resolver.
   373  	return rc.WriteFile(sb.config.resolvConfPath, "", filePerm)
   374  }
   375  
   376  func createBasePath(dir string) error {
   377  	return os.MkdirAll(dir, dirPerm)
   378  }
   379  
   380  func copyFile(src, dst string) error {
   381  	sBytes, err := os.ReadFile(src)
   382  	if err != nil {
   383  		return err
   384  	}
   385  	return os.WriteFile(dst, sBytes, filePerm)
   386  }