moby/libnetwork/sandbox_dns_unix.go
Rob Murray 6c68be24a2 Windows DNS resolver forwarding
Make the internal DNS resolver for Windows containers forward requests
to upsteam DNS servers when it cannot respond itself, rather than
returning SERVFAIL.

Windows containers are normally configured with the internal resolver
first for service discovery (container name lookup), then external
resolvers from '--dns' or the host's networking configuration.

When a tool like ping gets a SERVFAIL from the internal resolver, it
tries the other nameservers. But, nslookup does not, and with this
change it does not need to.

The internal resolver learns external server addresses from the
container's HNSEndpoint configuration, so it will use the same DNS
servers as processes in the container.

The internal resolver for Windows containers listens on the network's
gateway address, and each container may have a different set of external
DNS servers. So, the resolver uses the source address of the DNS request
to select external resolvers.

On Windows, daemon.json feature option 'windows-no-dns-proxy' can be used
to prevent the internal resolver from forwarding requests (restoring the
old behaviour).

Signed-off-by: Rob Murray <rob.murray@docker.com>
2024-04-16 18:57:28 +01:00

385 lines
12 KiB
Go

//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"
)
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() error {
if err := sb.buildHostsFile(); err != nil {
return errdefs.System(err)
}
for _, ep := range sb.Endpoints() {
if err := sb.updateHostsFile(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() error {
// 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(ifaceIPs []string) error {
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
}
// Check for IPv6 endpoints in this sandbox. If there are any, and the container has
// IPv6 enabled, upstream requests from the internal DNS resolver can be made from
// the container's namespace.
// TODO(robmry) - this can only check networks connected when the resolver is set up,
// the configuration won't be updated if the container gets an IPv6 address later.
ipv6 := false
for _, ep := range sb.endpoints {
if ep.network.enableIPv6 {
if en, ok := sb.ipv6Enabled(); ok {
ipv6 = en
}
break
}
}
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(ipv6, 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)
}