| package daemon // import "github.com/docker/docker/daemon" | |
| import ( | |
| "bufio" | |
| "context" | |
| "fmt" | |
| "io" | |
| "net" | |
| "os" | |
| "regexp" | |
| "strings" | |
| "sync" | |
| "github.com/containerd/log" | |
| "github.com/docker/docker/daemon/config" | |
| "github.com/docker/docker/libnetwork/ns" | |
| "github.com/docker/docker/libnetwork/resolvconf" | |
| "github.com/moby/sys/mount" | |
| "github.com/moby/sys/mountinfo" | |
| "github.com/pkg/errors" | |
| "github.com/vishvananda/netlink" | |
| "golang.org/x/sys/unix" | |
| ) | |
| // On Linux, plugins use a static path for storing execution state, | |
| // instead of deriving path from daemon's exec-root. This is because | |
| // plugin socket files are created here and they cannot exceed max | |
| // path length of 108 bytes. | |
| func getPluginExecRoot(_ *config.Config) string { | |
| return "/run/docker/plugins" | |
| } | |
| func (daemon *Daemon) cleanupMountsByID(id string) error { | |
| log.G(context.TODO()).Debugf("Cleaning up old mountid %s: start.", id) | |
| f, err := os.Open("/proc/self/mountinfo") | |
| if err != nil { | |
| return err | |
| } | |
| defer f.Close() | |
| return daemon.cleanupMountsFromReaderByID(f, id, mount.Unmount) | |
| } | |
| func (daemon *Daemon) cleanupMountsFromReaderByID(reader io.Reader, id string, unmount func(target string) error) error { | |
| if daemon.root == "" { | |
| return nil | |
| } | |
| var errs []string | |
| regexps := getCleanPatterns(id) | |
| sc := bufio.NewScanner(reader) | |
| for sc.Scan() { | |
| if fields := strings.Fields(sc.Text()); len(fields) > 4 { | |
| if mnt := fields[4]; strings.HasPrefix(mnt, daemon.root) { | |
| for _, p := range regexps { | |
| if p.MatchString(mnt) { | |
| if err := unmount(mnt); err != nil { | |
| log.G(context.TODO()).Error(err) | |
| errs = append(errs, err.Error()) | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| if err := sc.Err(); err != nil { | |
| return err | |
| } | |
| if len(errs) > 0 { | |
| return fmt.Errorf("Error cleaning up mounts:\n%v", strings.Join(errs, "\n")) | |
| } | |
| log.G(context.TODO()).Debugf("Cleaning up old mountid %v: done.", id) | |
| return nil | |
| } | |
| // cleanupMounts umounts used by container resources and the daemon root mount | |
| func (daemon *Daemon) cleanupMounts(cfg *config.Config) error { | |
| if err := daemon.cleanupMountsByID(""); err != nil { | |
| return err | |
| } | |
| info, err := mountinfo.GetMounts(mountinfo.SingleEntryFilter(daemon.root)) | |
| if err != nil { | |
| return errors.Wrap(err, "error reading mount table for cleanup") | |
| } | |
| if len(info) < 1 { | |
| // no mount found, we're done here | |
| return nil | |
| } | |
| // `info.Root` here is the root mountpoint of the passed in path (`daemon.root`). | |
| // The ony cases that need to be cleaned up is when the daemon has performed a | |
| // `mount --bind /daemon/root /daemon/root && mount --make-shared /daemon/root` | |
| // This is only done when the daemon is started up and `/daemon/root` is not | |
| // already on a shared mountpoint. | |
| if !shouldUnmountRoot(daemon.root, info[0]) { | |
| return nil | |
| } | |
| unmountFile := getUnmountOnShutdownPath(cfg) | |
| if _, err := os.Stat(unmountFile); err != nil { | |
| return nil | |
| } | |
| log.G(context.TODO()).WithField("mountpoint", daemon.root).Debug("unmounting daemon root") | |
| if err := mount.Unmount(daemon.root); err != nil { | |
| return err | |
| } | |
| return os.Remove(unmountFile) | |
| } | |
| func getCleanPatterns(id string) (regexps []*regexp.Regexp) { | |
| var patterns []string | |
| if id == "" { | |
| id = "[0-9a-f]{64}" | |
| patterns = append(patterns, "containers/"+id+"/mounts/shm", "containers/"+id+"/shm") | |
| } | |
| patterns = append(patterns, "overlay2/"+id+"/merged$", "zfs/graph/"+id+"$") | |
| for _, p := range patterns { | |
| r, err := regexp.Compile(p) | |
| if err == nil { | |
| regexps = append(regexps, r) | |
| } | |
| } | |
| return | |
| } | |
| func shouldUnmountRoot(root string, info *mountinfo.Info) bool { | |
| if !strings.HasSuffix(root, info.Root) { | |
| return false | |
| } | |
| return hasMountInfoOption(info.Optional, sharedPropagationOption) | |
| } | |
| // setupResolvConf sets the appropriate resolv.conf file if not specified | |
| // When systemd-resolved is running the default /etc/resolv.conf points to | |
| // localhost. In this case fetch the alternative config file that is in a | |
| // different path so that containers can use it | |
| // In all the other cases fallback to the default one | |
| func setupResolvConf(config *config.Config) { | |
| if config.ResolvConf != "" { | |
| return | |
| } | |
| config.ResolvConf = resolvconf.Path() | |
| } | |
| // ifaceAddrs returns the IPv4 and IPv6 addresses assigned to the network | |
| // interface with name linkName. | |
| // | |
| // No error is returned if the named interface does not exist. | |
| func ifaceAddrs(linkName string) (v4, v6 []*net.IPNet, err error) { | |
| nl := ns.NlHandle() | |
| link, err := nl.LinkByName(linkName) | |
| if err != nil { | |
| if !errors.As(err, new(netlink.LinkNotFoundError)) { | |
| return nil, nil, err | |
| } | |
| return nil, nil, nil | |
| } | |
| get := func(family int) ([]*net.IPNet, error) { | |
| addrs, err := nl.AddrList(link, family) | |
| if err != nil { | |
| return nil, err | |
| } | |
| ipnets := make([]*net.IPNet, len(addrs)) | |
| for i := range addrs { | |
| ipnets[i] = addrs[i].IPNet | |
| } | |
| return ipnets, nil | |
| } | |
| v4, err = get(netlink.FAMILY_V4) | |
| if err != nil { | |
| return nil, nil, err | |
| } | |
| v6, err = get(netlink.FAMILY_V6) | |
| if err != nil { | |
| return nil, nil, err | |
| } | |
| return v4, v6, nil | |
| } | |
| var ( | |
| kernelSupportsRROOnce sync.Once | |
| kernelSupportsRROErr error | |
| ) | |
| func kernelSupportsRecursivelyReadOnly() error { | |
| fn := func() error { | |
| tmpMnt, err := os.MkdirTemp("", "moby-detect-rro") | |
| if err != nil { | |
| return fmt.Errorf("failed to create a temp directory: %w", err) | |
| } | |
| for { | |
| err = unix.Mount("", tmpMnt, "tmpfs", 0, "") | |
| if !errors.Is(err, unix.EINTR) { | |
| break | |
| } | |
| } | |
| if err != nil { | |
| return fmt.Errorf("failed to mount tmpfs on %q: %w", tmpMnt, err) | |
| } | |
| defer func() { | |
| var umErr error | |
| for { | |
| umErr = unix.Unmount(tmpMnt, 0) | |
| if !errors.Is(umErr, unix.EINTR) { | |
| break | |
| } | |
| } | |
| if umErr != nil { | |
| log.G(context.TODO()).WithError(umErr).Warnf("Failed to unmount %q", tmpMnt) | |
| } | |
| }() | |
| attr := &unix.MountAttr{ | |
| Attr_set: unix.MOUNT_ATTR_RDONLY, | |
| } | |
| for { | |
| err = unix.MountSetattr(-1, tmpMnt, unix.AT_RECURSIVE, attr) | |
| if !errors.Is(err, unix.EINTR) { | |
| break | |
| } | |
| } | |
| // ENOSYS on kernel < 5.12 | |
| if err != nil { | |
| return fmt.Errorf("failed to call mount_setattr: %w", err) | |
| } | |
| return nil | |
| } | |
| kernelSupportsRROOnce.Do(func() { | |
| kernelSupportsRROErr = fn() | |
| }) | |
| return kernelSupportsRROErr | |
| } | |
| func supportsRecursivelyReadOnly(cfg *configStore, runtime string) error { | |
| if err := kernelSupportsRecursivelyReadOnly(); err != nil { | |
| return fmt.Errorf("rro is not supported: %w (kernel is older than 5.12?)", err) | |
| } | |
| if runtime == "" { | |
| runtime = cfg.Runtimes.Default | |
| } | |
| features := cfg.Runtimes.Features(runtime) | |
| if features == nil { | |
| return fmt.Errorf("rro is not supported by runtime %q: OCI features struct is not available", runtime) | |
| } | |
| for _, s := range features.MountOptions { | |
| if s == "rro" { | |
| return nil | |
| } | |
| } | |
| return fmt.Errorf("rro is not supported by runtime %q", runtime) | |
| } |