| //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 |
| } |
| |
| // 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) |
| } |