mirror of
https://github.com/moby/moby.git
synced 2026-01-11 18:51:37 +00:00
Since commit 925b484 ("No fallback nameservers for internal
resolver"), if the host's resolv.conf has no nameservers and
no servers are supplied via config, the internal resolver will
not use Google's DNS - so the container will not be able to
resolve external DNS requests.
That can happen when container's are "restart-always" and the
docker daemon starts before the host's DNS is configured.
So, to highlight the issue (which may not be an error, but
probably is), include a warning in the container's resolv.conf
file.
Also, log a warning - logs currently say "No non-localhost DNS
nameservers are left in resolv.conf. Using default external
servers". But, that's misleading because it's from an initial
resolv.conf setup, before the internal resolver configured without
those fallbacks - we'll drop the fallbacks completely once the
default bridge has an internal resolver).
Signed-off-by: Rob Murray <rob.murray@docker.com>
367 lines
12 KiB
Go
367 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"
|
|
"go.opentelemetry.io/otel"
|
|
)
|
|
|
|
const (
|
|
defaultPrefix = "/var/lib/docker/network/files"
|
|
dirPerm = 0o755
|
|
filePerm = 0o644
|
|
|
|
resolverIPSandbox = "127.0.0.11"
|
|
)
|
|
|
|
// AddHostsEntry adds an entry to /etc/hosts.
|
|
func (sb *Sandbox) AddHostsEntry(ctx context.Context, name, ip string) error {
|
|
sb.config.extraHosts = append(sb.config.extraHosts, extraHost{name: name, IP: ip})
|
|
return sb.rebuildHostsFile(ctx)
|
|
}
|
|
|
|
// UpdateHostsEntry updates the IP address in a /etc/hosts entry where the
|
|
// name matches the regular expression regexp.
|
|
func (sb *Sandbox) UpdateHostsEntry(regexp, ip string) error {
|
|
return etchosts.Update(sb.config.hostsPath, ip, regexp)
|
|
}
|
|
|
|
// rebuildHostsFile builds the container's /etc/hosts file, based on the current
|
|
// state of the Sandbox (including extra hosts). If called after the container
|
|
// namespace has been created, before the user process is started, the container's
|
|
// support for IPv6 can be determined and IPv6 hosts will be included/excluded
|
|
// accordingly.
|
|
func (sb *Sandbox) rebuildHostsFile(ctx context.Context) error {
|
|
var ifaceIPs []netip.Addr
|
|
for _, ep := range sb.Endpoints() {
|
|
ifaceIPs = append(ifaceIPs, ep.getEtcHostsAddrs()...)
|
|
}
|
|
if err := sb.buildHostsFile(ctx, ifaceIPs); 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(ctx, nil); err != nil {
|
|
return err
|
|
}
|
|
|
|
return sb.setupDNS()
|
|
}
|
|
|
|
func (sb *Sandbox) buildHostsFile(ctx context.Context, ifaceIPs []netip.Addr) error {
|
|
ctx, span := otel.Tracer("").Start(ctx, "libnetwork.buildHostsFile")
|
|
defer span.End()
|
|
|
|
sb.restoreHostsPath()
|
|
|
|
dir, _ := filepath.Split(sb.config.hostsPath)
|
|
if err := createBasePath(dir); err != nil {
|
|
return err
|
|
}
|
|
|
|
// This is for the host mode networking. If extra hosts are supplied, even though
|
|
// it's host-networking, the container's hosts file is not based on the host's -
|
|
// so that it's possible to override a hostname that's in the host's hosts file.
|
|
// See analysis of how this came about in:
|
|
// https://github.com/moby/moby/pull/48823#issuecomment-2461777129
|
|
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)+len(ifaceIPs))
|
|
for _, host := range sb.config.extraHosts {
|
|
addr, err := netip.ParseAddr(host.IP)
|
|
if err != nil {
|
|
return errdefs.InvalidParameter(fmt.Errorf("could not parse extra host IP %s: %v", host.IP, err))
|
|
}
|
|
extraContent = append(extraContent, etchosts.Record{Hosts: host.name, IP: addr})
|
|
}
|
|
extraContent = append(extraContent, sb.makeHostsRecs(ifaceIPs)...)
|
|
|
|
// Assume IPv6 support, unless it's definitely disabled.
|
|
if en, ok := sb.IPv6Enabled(); ok && !en {
|
|
return etchosts.BuildNoIPv6(sb.config.hostsPath, extraContent)
|
|
}
|
|
return etchosts.Build(sb.config.hostsPath, extraContent)
|
|
}
|
|
|
|
func (sb *Sandbox) makeHostsRecs(ifaceIPs []netip.Addr) []etchosts.Record {
|
|
if len(ifaceIPs) == 0 {
|
|
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.
|
|
hosts := sb.config.hostName
|
|
if sb.config.domainName != "" {
|
|
hosts += "." + sb.config.domainName
|
|
}
|
|
|
|
if hn, _, ok := strings.Cut(hosts, "."); ok {
|
|
hosts += " " + hn
|
|
}
|
|
|
|
var recs []etchosts.Record
|
|
for _, ip := range ifaceIPs {
|
|
recs = append(recs, etchosts.Record{Hosts: hosts, IP: ip})
|
|
}
|
|
return recs
|
|
}
|
|
|
|
func (sb *Sandbox) addHostsEntries(ctx context.Context, ifaceAddrs []netip.Addr) {
|
|
ctx, span := otel.Tracer("").Start(ctx, "libnetwork.addHostsEntries")
|
|
defer span.End()
|
|
|
|
// Assume IPv6 support, unless it's definitely disabled.
|
|
if en, ok := sb.IPv6Enabled(); ok && !en {
|
|
var filtered []netip.Addr
|
|
for _, addr := range ifaceAddrs {
|
|
if !addr.Is6() {
|
|
filtered = append(filtered, addr)
|
|
}
|
|
}
|
|
ifaceAddrs = filtered
|
|
}
|
|
if err := etchosts.Add(sb.config.hostsPath, sb.makeHostsRecs(ifaceAddrs)); err != nil {
|
|
log.G(context.TODO()).Warnf("Failed adding service host entries to the running container: %v", err)
|
|
}
|
|
}
|
|
|
|
func (sb *Sandbox) deleteHostsEntries(ifaceAddrs []netip.Addr) {
|
|
if err := etchosts.Delete(sb.config.hostsPath, sb.makeHostsRecs(ifaceAddrs)); err != nil {
|
|
log.G(context.TODO()).Warnf("Failed deleting service host entries to the running container: %v", err)
|
|
}
|
|
}
|
|
|
|
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) {
|
|
if len(entries) == 0 {
|
|
log.G(context.TODO()).WithField("cid", sb.ContainerID()).Warn("DNS resolver has no external nameservers")
|
|
sb.extDNS = nil
|
|
return
|
|
}
|
|
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)
|
|
}
|