dotcloud/docker

View on GitHub
libnetwork/sandbox_dns_unix.go

Summary

Maintainability
A
0 mins
Test Coverage
//go:build !windows

package libnetwork

import (
    "context"
    "fmt"
    "io/fs"
    "net/netip"
    "os"
    "path/filepath"
    "strings"

    "github.com/containerd/log"
    "github.com/docker/docker/errdefs"
    "github.com/docker/docker/libnetwork/etchosts"
    "github.com/docker/docker/libnetwork/internal/resolvconf"
    "github.com/docker/docker/libnetwork/types"
    "github.com/pkg/errors"
    "go.opentelemetry.io/otel"
)

const (
    defaultPrefix = "/var/lib/docker/network/files"
    dirPerm       = 0o755
    filePerm      = 0o644

    resolverIPSandbox = "127.0.0.11"
)

// finishInitDNS is to be called after the container namespace has been created,
// before it the user process is started. The container's support for IPv6 can be
// determined at this point.
func (sb *Sandbox) finishInitDNS(ctx context.Context) error {
    if err := sb.buildHostsFile(); err != nil {
        return errdefs.System(err)
    }
    for _, ep := range sb.Endpoints() {
        if err := sb.updateHostsFile(ctx, ep.getEtcHostsAddrs()); err != nil {
            return errdefs.System(err)
        }
    }
    return nil
}

func (sb *Sandbox) startResolver(restore bool) {
    sb.resolverOnce.Do(func() {
        var err error
        // The resolver is started with proxyDNS=false if the sandbox does not currently
        // have a gateway. So, if the Sandbox is only connected to an 'internal' network,
        // it will not forward DNS requests to external resolvers. The resolver's
        // proxyDNS setting is then updated as network Endpoints are added/removed.
        sb.resolver = NewResolver(resolverIPSandbox, sb.hasExternalAccess(), sb)
        defer func() {
            if err != nil {
                sb.resolver = nil
            }
        }()

        // In the case of live restore container is already running with
        // right resolv.conf contents created before. Just update the
        // external DNS servers from the restored sandbox for embedded
        // server to use.
        if !restore {
            err = sb.rebuildDNS()
            if err != nil {
                log.G(context.TODO()).Errorf("Updating resolv.conf failed for container %s, %q", sb.ContainerID(), err)
                return
            }
        }
        sb.resolver.SetExtServers(sb.extDNS)

        if err = sb.osSbox.InvokeFunc(sb.resolver.SetupFunc(0)); err != nil {
            log.G(context.TODO()).Errorf("Resolver Setup function failed for container %s, %q", sb.ContainerID(), err)
            return
        }

        if err = sb.resolver.Start(); err != nil {
            log.G(context.TODO()).Errorf("Resolver Start failed for container %s, %q", sb.ContainerID(), err)
        }
    })
}

func (sb *Sandbox) setupResolutionFiles(ctx context.Context) error {
    _, span := otel.Tracer("").Start(ctx, "libnetwork.Sandbox.setupResolutionFiles")
    defer span.End()

    // Create a hosts file that can be mounted during container setup. For most
    // networking modes (not host networking) it will be re-created before the
    // container start, once its support for IPv6 is known.
    if sb.config.hostsPath == "" {
        sb.config.hostsPath = defaultPrefix + "/" + sb.id + "/hosts"
    }
    dir, _ := filepath.Split(sb.config.hostsPath)
    if err := createBasePath(dir); err != nil {
        return err
    }
    if err := sb.buildHostsFile(); err != nil {
        return err
    }

    return sb.setupDNS()
}

func (sb *Sandbox) buildHostsFile() error {
    sb.restoreHostsPath()

    dir, _ := filepath.Split(sb.config.hostsPath)
    if err := createBasePath(dir); err != nil {
        return err
    }

    // This is for the host mode networking
    if sb.config.useDefaultSandBox && len(sb.config.extraHosts) == 0 {
        // We are working under the assumption that the origin file option had been properly expressed by the upper layer
        // if not here we are going to error out
        if err := copyFile(sb.config.originHostsPath, sb.config.hostsPath); err != nil && !os.IsNotExist(err) {
            return types.InternalErrorf("could not copy source hosts file %s to %s: %v", sb.config.originHostsPath, sb.config.hostsPath, err)
        }
        return nil
    }

    extraContent := make([]etchosts.Record, 0, len(sb.config.extraHosts))
    for _, extraHost := range sb.config.extraHosts {
        extraContent = append(extraContent, etchosts.Record{Hosts: extraHost.name, IP: extraHost.IP})
    }

    // Assume IPv6 support, unless it's definitely disabled.
    buildf := etchosts.Build
    if en, ok := sb.ipv6Enabled(); ok && !en {
        buildf = etchosts.BuildNoIPv6
    }
    if err := buildf(sb.config.hostsPath, extraContent); err != nil {
        return err
    }

    return sb.updateParentHosts()
}

func (sb *Sandbox) updateHostsFile(ctx context.Context, ifaceIPs []string) error {
    ctx, span := otel.Tracer("").Start(ctx, "libnetwork.updateHostsFile")
    defer span.End()

    if len(ifaceIPs) == 0 {
        return nil
    }

    if sb.config.originHostsPath != "" {
        return nil
    }

    // User might have provided a FQDN in hostname or split it across hostname
    // and domainname.  We want the FQDN and the bare hostname.
    fqdn := sb.config.hostName
    if sb.config.domainName != "" {
        fqdn += "." + sb.config.domainName
    }
    hosts := fqdn

    if hostName, _, ok := strings.Cut(fqdn, "."); ok {
        hosts += " " + hostName
    }

    var extraContent []etchosts.Record
    for _, ip := range ifaceIPs {
        extraContent = append(extraContent, etchosts.Record{Hosts: hosts, IP: ip})
    }

    sb.addHostsEntries(extraContent)
    return nil
}

func (sb *Sandbox) addHostsEntries(recs []etchosts.Record) {
    // Assume IPv6 support, unless it's definitely disabled.
    if en, ok := sb.ipv6Enabled(); ok && !en {
        var filtered []etchosts.Record
        for _, rec := range recs {
            if addr, err := netip.ParseAddr(rec.IP); err == nil && !addr.Is6() {
                filtered = append(filtered, rec)
            }
        }
        recs = filtered
    }
    if err := etchosts.Add(sb.config.hostsPath, recs); err != nil {
        log.G(context.TODO()).Warnf("Failed adding service host entries to the running container: %v", err)
    }
}

func (sb *Sandbox) deleteHostsEntries(recs []etchosts.Record) {
    if err := etchosts.Delete(sb.config.hostsPath, recs); err != nil {
        log.G(context.TODO()).Warnf("Failed deleting service host entries to the running container: %v", err)
    }
}

func (sb *Sandbox) updateParentHosts() error {
    var pSb *Sandbox

    for _, update := range sb.config.parentUpdates {
        // 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.
        if s, _ := sb.controller.GetSandbox(update.cid); s != nil {
            pSb = s
        }
        if pSb == nil {
            continue
        }
        // TODO(robmry) - filter out IPv6 addresses here if !sb.ipv6Enabled() but...
        // - this is part of the implementation of '--link', which will be removed along
        //   with the rest of legacy networking.
        // - IPv6 addresses shouldn't be allocated if IPv6 is not available in a container,
        //   and that change will come along later.
        // - I think this may be dead code, it's not possible to start a parent container with
        //   '--link child' unless the child has already started ("Error response from daemon:
        //   Cannot link to a non running container"). So, when the child starts and this method
        //   is called with updates for parents, the parents aren't running and GetSandbox()
        //   returns nil.)
        if err := etchosts.Update(pSb.config.hostsPath, update.ip, update.name); err != nil {
            return err
        }
    }

    return nil
}

func (sb *Sandbox) restoreResolvConfPath() {
    if sb.config.resolvConfPath == "" {
        sb.config.resolvConfPath = defaultPrefix + "/" + sb.id + "/resolv.conf"
    }
    sb.config.resolvConfHashFile = sb.config.resolvConfPath + ".hash"
}

func (sb *Sandbox) restoreHostsPath() {
    if sb.config.hostsPath == "" {
        sb.config.hostsPath = defaultPrefix + "/" + sb.id + "/hosts"
    }
}

func (sb *Sandbox) setExternalResolvers(entries []resolvconf.ExtDNSEntry) {
    sb.extDNS = make([]extDNSEntry, 0, len(entries))
    for _, entry := range entries {
        sb.extDNS = append(sb.extDNS, extDNSEntry{
            IPStr:        entry.Addr.String(),
            HostLoopback: entry.HostLoopback,
        })
    }
}

func (c *containerConfig) getOriginResolvConfPath() string {
    if c.originResolvConfPath != "" {
        return c.originResolvConfPath
    }
    // Fallback if not specified.
    return resolvconf.Path()
}

// loadResolvConf reads the resolv.conf file at path, and merges in overrides for
// nameservers, options, and search domains.
func (sb *Sandbox) loadResolvConf(path string) (*resolvconf.ResolvConf, error) {
    rc, err := resolvconf.Load(path)
    if err != nil && !errors.Is(err, fs.ErrNotExist) {
        return nil, err
    }
    // Proceed with rc, which might be zero-valued if path does not exist.

    rc.SetHeader(`# Generated by Docker Engine.
# This file can be edited; Docker Engine will not make further changes once it
# has been modified.`)
    if len(sb.config.dnsList) > 0 {
        var dnsAddrs []netip.Addr
        for _, ns := range sb.config.dnsList {
            addr, err := netip.ParseAddr(ns)
            if err != nil {
                return nil, errors.Wrapf(err, "bad nameserver address %s", ns)
            }
            dnsAddrs = append(dnsAddrs, addr)
        }
        rc.OverrideNameServers(dnsAddrs)
    }
    if len(sb.config.dnsSearchList) > 0 {
        rc.OverrideSearch(sb.config.dnsSearchList)
    }
    if len(sb.config.dnsOptionsList) > 0 {
        rc.OverrideOptions(sb.config.dnsOptionsList)
    }
    return &rc, nil
}

// For a new sandbox, write an initial version of the container's resolv.conf. It'll
// be a copy of the host's file, with overrides for nameservers, options and search
// domains applied.
func (sb *Sandbox) setupDNS() error {
    // Make sure the directory exists.
    sb.restoreResolvConfPath()
    dir, _ := filepath.Split(sb.config.resolvConfPath)
    if err := createBasePath(dir); err != nil {
        return err
    }

    rc, err := sb.loadResolvConf(sb.config.getOriginResolvConfPath())
    if err != nil {
        return err
    }
    return rc.WriteFile(sb.config.resolvConfPath, sb.config.resolvConfHashFile, filePerm)
}

// Called when an endpoint has joined the sandbox.
func (sb *Sandbox) updateDNS(ipv6Enabled bool) error {
    if mod, err := resolvconf.UserModified(sb.config.resolvConfPath, sb.config.resolvConfHashFile); err != nil || mod {
        return err
    }

    // Load the host's resolv.conf as a starting point.
    rc, err := sb.loadResolvConf(sb.config.getOriginResolvConfPath())
    if err != nil {
        return err
    }
    // For host-networking, no further change is needed.
    if !sb.config.useDefaultSandBox {
        // The legacy bridge network has no internal nameserver. So, strip localhost
        // nameservers from the host's config, then add default nameservers if there
        // are none remaining.
        rc.TransformForLegacyNw(ipv6Enabled)
    }
    return rc.WriteFile(sb.config.resolvConfPath, sb.config.resolvConfHashFile, filePerm)
}

// Embedded DNS server has to be enabled for this sandbox. Rebuild the container's resolv.conf.
func (sb *Sandbox) rebuildDNS() error {
    // Don't touch the file if the user has modified it.
    if mod, err := resolvconf.UserModified(sb.config.resolvConfPath, sb.config.resolvConfHashFile); err != nil || mod {
        return err
    }

    // Load the host's resolv.conf as a starting point.
    rc, err := sb.loadResolvConf(sb.config.getOriginResolvConfPath())
    if err != nil {
        return err
    }

    intNS := sb.resolver.NameServer()
    if !intNS.IsValid() {
        return fmt.Errorf("no listen-address for internal resolver")
    }

    // Work out whether ndots has been set from host config or overrides.
    _, sb.ndotsSet = rc.Option("ndots")
    // Swap nameservers for the internal one, and make sure the required options are set.
    var extNameServers []resolvconf.ExtDNSEntry
    extNameServers, err = rc.TransformForIntNS(intNS, sb.resolver.ResolverOptions())
    if err != nil {
        return err
    }
    // Extract the list of nameservers that just got swapped out, and store them as
    // upstream nameservers.
    sb.setExternalResolvers(extNameServers)

    // Write the file for the container - preserving old behaviour, not updating the
    // hash file (so, no further updates will be made).
    // TODO(robmry) - I think that's probably accidental, I can't find a reason for it,
    //  and the old resolvconf.Build() function wrote the file but not the hash, which
    //  is surprising. But, before fixing it, a guard/flag needs to be added to
    //  sb.updateDNS() to make sure that when an endpoint joins a sandbox that already
    //  has an internal resolver, the container's resolv.conf is still (re)configured
    //  for an internal resolver.
    return rc.WriteFile(sb.config.resolvConfPath, "", filePerm)
}

func createBasePath(dir string) error {
    return os.MkdirAll(dir, dirPerm)
}

func copyFile(src, dst string) error {
    sBytes, err := os.ReadFile(src)
    if err != nil {
        return err
    }
    return os.WriteFile(dst, sBytes, filePerm)
}