docker cp源码分析

docker cp源码分析

最近在分析docker的几个cve的时候发现好多都与docker cp命令有关,故此从代码层面分析一下docker cp的实现流程

docker cp简介

docker cp命令用于宿主机和容器之间的文件传输。数据流向有两种,从容器到宿主机和从宿主机到容器,目前不支持从容器到容器的复制。命令格式为:

docker cp hostfile containerID:containerFile #从宿主机向容器复制文件
docker cp containerID:containerFile hostFile #从容器向宿主机复制文件

docker client源码分析

当用户执行docker cp命令后会首先进入runCopy函数(/cli/command/container/cp.go)

func runCopy(dockerCli command.Cli, opts copyOptions) error {
    srcContainer, srcPath := splitCpArg(opts.source)
    destContainer, destPath := splitCpArg(opts.destination)
    ...
    var direction copyDirection
    if srcContainer != "" {
        direction |= fromContainer
        copyConfig.container = srcContainer
    }
    if destContainer != "" {
        direction |= toContainer
        copyConfig.container = destContainer
    }
    ...
    switch direction {
    case fromContainer:
        return copyFromContainer(ctx, dockerCli, copyConfig)
    case toContainer:
        return copyToContainer(ctx, dockerCli, copyConfig)
    case acrossContainers:
        return errors.New("copying between containers is not supported")
    default:
        return errors.New("must specify at least one container source")
    }
}

runCopy函数首先对用户输入的参数进行解析,判断是从宿主机向容器复制文件还是从容器向宿主机复制文件。根据文件流向的不同调用不同的函数。

copyFromContainer

当用户执行docker cp containerid:containerFile hostFile命令时会进入copyFromContainer函数。

func copyFromContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpConfig) (err error) {
    ...
    client := dockerCli.Client()
    // if client requests to follow symbol link, then must decide target file to be copied
    var rebaseName string
    if copyConfig.followLink {
        srcStat, err := client.ContainerStatPath(ctx, copyConfig.container, srcPath)

        // If the destination is a symbolic link, we should follow it.
        if err == nil && srcStat.Mode&os.ModeSymlink != 0 {
            linkTarget := srcStat.LinkTarget
            if !system.IsAbs(linkTarget) {
                // Join with the parent directory.
                srcParent, _ := archive.SplitPathDirEntry(srcPath)
                linkTarget = filepath.Join(srcParent, linkTarget)
            }

            linkTarget, rebaseName = archive.GetRebaseName(srcPath, linkTarget)
            srcPath = linkTarget
        }

    }
    ...
    content, stat, err := client.CopyFromContainer(ctx, copyConfig.container, srcPath)
    ...
    preArchive := content
    if len(srcInfo.RebaseName) != 0 {
        _, srcBase := archive.SplitPathDirEntry(srcInfo.Path)
        preArchive = archive.RebaseArchiveEntries(content, srcBase, srcInfo.RebaseName)
    }
    ...
    res := archive.CopyTo(preArchive, srcInfo, dstPath)
    ...
    return res
}

如果设置了followLink,copyFromContainer函数会先调用client.ContainerStatPath函数向docker daemon的HEAD /containers/(containerID)/archive接口发送文件路径信息和容器信息来获得srcPath的链接target,并将srcPath(也就时容器中的文件位置)设置为链接的目标文件,同时设置rebasename表示源文件路径需要调整。

func (cli *Client) ContainerStatPath(ctx context.Context, containerID, path string) (types.ContainerPathStat, error) {
    ...
    urlStr := "/containers/" + containerID + "/archive"
    response, err := cli.head(ctx, urlStr, query, nil)
    ...
}

之后调用client.CopyFromContainer函数将srcPath以及container的信息发送给docker daemon的GET /containers/(containerID)/archive接口。docker daemon的该接口会根据path和container信息将需要复制的文件进行打包,并以response的形式返回给docker client

func (cli *Client) CopyFromContainer(ctx context.Context, containerID, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
    ...
    apiPath := "/containers/" + containerID + "/archive"
    response, err := cli.get(ctx, apiPath, query, nil)
    ...
    return response.body, stat, err
}

接收到response中的文件信息后docker client会使用CopyTo函数将response中的文件移动到dstpath位置。在此之前,如果源文件的路径需要重新调整(rebaseName 不为空),则使用 archive.RebaseArchiveEntries 函数调整文件内容中的条目路径。
总体而言 copyFromContainer的执行流程大致如下图所示,其中红色标识的函数为client向daemon发送请求,其实际的执行逻辑由daemon进行:
docker cp源码分析_第1张图片

copyToContainer

当用户执行docker cp hostFile containerid:containerFile 命令时会进入copyToContainer函数。

func copyToContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpConfig) (err error) {
    ...
    client := dockerCli.Client()
     ...
    dstStat, err := client.ContainerStatPath(ctx, copyConfig.container, dstPath)
    if err == nil && dstStat.Mode&os.ModeSymlink != 0 {
        linkTarget := dstStat.LinkTarget
        if !system.IsAbs(linkTarget) {
            dstParent, _ := archive.SplitPathDirEntry(dstPath)
            linkTarget = filepath.Join(dstParent, linkTarget)
        }
        dstInfo.Path = linkTarget
        dstStat, err = client.ContainerStatPath(ctx, copyConfig.container, linkTarget)
    }
    ...
    if srcPath == "-" {
        content = os.Stdin
        resolvedDstPath = dstInfo.Path
        if !dstInfo.IsDir {
            return errors.Errorf("destination \"%s:%s\" must be a directory", copyConfig.container, dstPath)
        }
    } else {
        srcInfo, err := archive.CopyInfoSourcePath(srcPath, copyConfig.followLink)
        ...
        srcArchive, err := archive.TarResource(srcInfo)
        ...
        dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, dstInfo)
        ...
        resolvedDstPath = dstDir
        content = preparedArchive
        ...
    }
    ...
    res := client.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options)
    ...
}

和copyFromContainer类似,copyToContainer首先会对dstPath也就是容器内的目标地址进行评估,通过client.ContainerStatPath函数向docker daemon发送请求获取文件信息。如果目标路径是一个链接的话则对于链接的目标地址继续使用client.ContainerStatPath获取信息。
在获取到目标地址的信息后,copyToContainer函数使用archive.TarResource函数对宿主机的文件进行打包。最后通过client.CopyToContainer函数将打包后的文件,容器信息,目标地址信息发送给docker daemon的PUT /containers/(containerID)/archive接口。

func (cli *Client) CopyToContainer(ctx context.Context, containerID, dstPath string, content io.Reader, options types.CopyToContainerOptions) error {
    ...
    apiPath := "/containers/" + containerID + "/archive"
    response, err := cli.putRaw(ctx, apiPath, query, content, nil)
    ...
}

docker daemon接受到此信息后将request中的打包后的文件复制到容器内部。总体而言 copyToContainer的执行流程大致如下图所示,其中红色标识的函数为client向daemon发送请求,其实际的执行逻辑由daemon进行:
docker cp源码分析_第2张图片

docker client总结

通过上述分析可以发现,copyFromContainer和copyToContainer函数的处理逻辑是很相似的,只是打包文件的流向发生了变化,copyFromContainer是docker daemon->docker client。copyToContainerdocker client->docker daemon。也就是说docker cp中关于宿主机的文件操作(打包,解包)都是在docker client处进行的,关于容器的文件操作(打包,解包,获取文件信息)都是在docker daemon处进行的。

docker daemon 源码分析

在moby的 api/server/router/container/container.go中定义了如下接口:

func (r *containerRouter) initRoutes() {
    ...
    router.NewHeadRoute("/containers/{name:.*}/archive", r.headContainersArchive),
    router.NewGetRoute("/containers/{name:.*}/archive", r.getContainersArchive),
    router.NewPutRoute("/containers/{name:.*}/archive", r.putContainersArchive),
    ...
}

这些接口是对外提供给docker client的。通过上面的docker client源码分析我们可以了解到,第一个接口是用来获取容器内部文件信息的,第二个接口使用来从docker daemon向docker client发送打包文件的,第三个接口是用来从docker client向docker daemon发送打包文件的。下面对这些接口的函数做详细分析

headContainersArchive

func (s *containerRouter) headContainersArchive(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
    v, err := httputils.ArchiveFormValues(r, vars)
    ...
    stat, err := s.backend.ContainerStatPath(v.Name, v.Path)
    ...
    return setContainerPathStatHeader(stat, w.Header())
}

func (daemon *Daemon) ContainerStatPath(name string, path string) (stat *types.ContainerPathStat, err error) {
    ctr, err := daemon.GetContainer(name)
    ...
    stat, err = daemon.containerStatPath(ctr, path)
    ...
}

func (daemon *Daemon) containerStatPath(container *container.Container, path string) (stat *types.ContainerPathStat, err error) {
    ...
    cfs, err := daemon.openContainerFS(container)
    ...
    return cfs.Stat(context.TODO(), path)
}

headContainersArchive函数通过s.backend.ContainerStatPath调用到daemon.containerStatPath函数。daemon.containerStatPath函数首先打开对应容器的文件系统,并调用该文件系统的Stat函数获取文件信息。

func (vw *containerFSView) Stat(ctx context.Context, path string) (*types.ContainerPathStat, error) {
    var stat *types.ContainerPathStat
    err := vw.RunInFS(ctx, func() error {
        lstat, err := os.Lstat(path)
        ...
        var target string
        if lstat.Mode()&os.ModeSymlink != 0 {
            target, err = symlink.FollowSymlinkInScope(path, "/")
            ...
        }
        stat = &types.ContainerPathStat{
            Name:       filepath.Base(path),
            Size:       lstat.Size(),
            Mode:       lstat.Mode(),
            Mtime:      lstat.ModTime(),
            LinkTarget: target,
        }
        return nil
    })
    return stat, err
}

在Stat函数中,首先使用vw.RunInFS限定当前在容器的上下文环境中执行。关于文件的操作,首先使用os.Lstat获取对应文件的FileInfo信息,之后判断该文件是否为链接文件,如果是则使用FollowSymlinkInScope函数获取此链接文件的信息

func FollowSymlinkInScope(path, root string) (string, error) {
    path, err := filepath.Abs(filepath.FromSlash(path))
    if err != nil {
        return "", err
    }
    root, err = filepath.Abs(filepath.FromSlash(root))
    if err != nil {
        return "", err
    }
    return evalSymlinksInScope(path, root)
}

FollowSymlinkInScope实际上是调用evalSymlinksInScope进行实际的处理逻辑的,evalSymlinksInScope 将在调用时对作用域 rootpath 的符号链接进行评估,并返回保证包含在作用域 root 中的结果。例如:/foo/bar链接指向/outside,那么evalSymlinksInScope(“/foo/bar”,“/foo”)会返回/foo/outside。

getContainersArchive

func (s *containerRouter) getContainersArchive(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
    v, err := httputils.ArchiveFormValues(r, vars)
    ...
    tarArchive, stat, err := s.backend.ContainerArchivePath(v.Name, v.Path)
    ...
    return writeCompressedResponse(w, r, tarArchive)
}

func (daemon *Daemon) ContainerArchivePath(name string, path string) (content io.ReadCloser, stat *types.ContainerPathStat, err error) {
    ctr, err := daemon.GetContainer(name)
    ...
    content, stat, err = daemon.containerArchivePath(ctr, path)
    ...
}

当docker daemon检测到"GET /containers/(containerID)/archive"接口有消息时会进入到getContainersArchive函数。getContainersArchive函数会经过s.backend.ContainerArchivePath函数调用到daemon.containerArchivePath函数。

func (daemon *Daemon) containerArchivePath(container *container.Container, path string) (content io.ReadCloser, stat *types.ContainerPathStat, err error) {
    cfs, err := daemon.openContainerFS(container)
    absPath := archive.PreserveTrailingDotOrSeparator(filepath.Join("/", path), path)

    stat, err = cfs.Stat(context.TODO(), absPath)
    ...
    sourceDir, sourceBase := absPath, "."
    if stat.Mode&os.ModeDir == 0 { // not dir
        sourceDir, sourceBase = filepath.Split(absPath)
    }
    opts := archive.TarResourceRebaseOpts(sourceBase, filepath.Base(absPath))

    tb, err := archive.NewTarballer(sourceDir, opts)
    ...
    cfs.GoInFS(context.TODO(), tb.Do)
    data := tb.Reader()
    content = ioutils.NewReadCloserWrapper(data, func() error {
        err := data.Close()
        _ = cfs.Close()
        container.Unlock()
        return err
    })
    ...
    return content, stat, nil
}

该函数首先通过容器信息打开一个容器的文件系统,使用Stat函数获取要打包的文件信息。如果该文件不是路径类文件,则sourceDir,sourceBase分别为该文件的路径和文件名,否则sourceDir和sourceBase分别为该文件的路径和“.“。例如:要打包的文件为/etc/passwd,那么sourceDir为/etc/,sourceBase为passwd;要打包的文件为/etc/,那么sourceDir为/etc/,sourceBase为“.”

cfs, err := daemon.openContainerFS(container)
    absPath := archive.PreserveTrailingDotOrSeparator(filepath.Join("/", path), path)

    stat, err = cfs.Stat(context.TODO(), absPath)
    ...
    sourceDir, sourceBase := absPath, "."
    if stat.Mode&os.ModeDir == 0 { // not dir
        sourceDir, sourceBase = filepath.Split(absPath)
    }

之后根据sourceDir和opts(由sourceBase生成的用来指示打包信息的选项)生成一个新的Tarballer对象。

opts := archive.TarResourceRebaseOpts(sourceBase, filepath.Base(absPath))
tb, err := archive.NewTarballer(sourceDir, opts)

下面就要进行文件打包的操作了,首先会使用GoInFS将该操作限定在容器的上下文环境中,在容器的上下文环境中使用tb.Do进行具体的打包逻辑,最后将打包好的文件返回。

cfs.GoInFS(context.TODO(), tb.Do)
    data := tb.Reader()
    content = ioutils.NewReadCloserWrapper(data, func() error {
        err := data.Close()
        _ = cfs.Close()
        container.Unlock()
        return err
    })
    ...
    return content, stat, nil

putContainersArchive

func (s *containerRouter) putContainersArchive(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
    v, err := httputils.ArchiveFormValues(r, vars)
    ...
    return s.backend.ContainerExtractToDir(v.Name, v.Path, copyUIDGID, noOverwriteDirNonDir, r.Body)
}

func (daemon *Daemon) ContainerExtractToDir(name, path string, copyUIDGID, noOverwriteDirNonDir bool, content io.Reader) error {
    ctr, err := daemon.GetContainer(name)
    ...
    err = daemon.containerExtractToDir(ctr, path, copyUIDGID, noOverwriteDirNonDir, content)
    ...
}

当docker daemon监听到PUT /containers/(containerID)/archive接口有请求时会进入putContainersArchive函数。该函数会通过s.backend.ContainerExtractToDir函数调用到daemon.containerExtractToDir函数。

func (daemon *Daemon) containerExtractToDir(container *container.Container, path string, copyUIDGID, noOverwriteDirNonDir bool, content io.Reader) (err error) {
    ...
    cfs, err := daemon.openContainerFS(container)
    ...
    err = cfs.RunInFS(context.TODO(), func() error {
        ...
        absPath, err := filepath.EvalSymlinks(filepath.Join("/", path))
        if err != nil {
            return err
        }
        absPath = archive.PreserveTrailingDotOrSeparator(absPath, path)

        stat, err := os.Lstat(absPath)
        if err != nil {
            return err
        }
        if !stat.IsDir() {
            return errdefs.InvalidParameter(errors.New("extraction point is not a directory"))
        }

        // Need to check if the path is in a volume. If it is, it cannot be in a
        // read-only volume. If it is not in a volume, the container cannot be
        // configured with a read-only rootfs.
        toVolume, err := checkIfPathIsInAVolume(container, absPath)
        if err != nil {
            return err
        }

        if !toVolume && container.HostConfig.ReadonlyRootfs {
            return errdefs.InvalidParameter(errors.New("container rootfs is marked read-only"))
        }

        options := daemon.defaultTarCopyOptions(noOverwriteDirNonDir)

        if copyUIDGID {
            var err error
            // tarCopyOptions will appropriately pull in the right uid/gid for the
            // user/group and will set the options.
            options, err = daemon.tarCopyOptions(container, noOverwriteDirNonDir)
            if err != nil {
                return err
            }
        }

        return archive.Untar(content, absPath, options)
    })
    if err != nil {
        return err
    }

    daemon.LogContainerEvent(container, "extract-to-dir")

    return nil
}

该函数首先通过容器信息打开对应容器的文件系统,之后使用RunInFS将文件解包的操作限制在容器的上下文环境之中。在文件解包时,docker daemon首先通过filepath.EvalSymlinks函数解析目标目录的绝对路径,在此过程中所有符号连接都会被解析。完成解析后,会使用os.Lstat函数获取文件的状态信息。

absPath, err := filepath.EvalSymlinks(filepath.Join("/", path))
        if err != nil {
            return err
        }
        absPath = archive.PreserveTrailingDotOrSeparator(absPath, path)

        stat, err := os.Lstat(absPath)
        if err != nil {
            return err
        }
        if !stat.IsDir() {
            return errdefs.InvalidParameter(errors.New("extraction point is not a directory"))
        }

之后会判断该目标路径是否位于一个只读卷或者只读的rootfs中,如果返回true则会报错。
此部分的具体处理逻辑为:
调用checkIfPathIsInAVolume函数判断目标路径是否在挂载卷中,如果是则把toVolume置为true并判断该卷是否为一个只读卷,如果是一个只读卷则返回false和一个err。
调用完该函数后对该函数的返回值做一个判断,如果err不为空则代表该目标路径位于一个只读卷中,则直接返回该err。否则使用toVolume判断目标路径是否在一个挂载卷中(此时如果toVolume为true那么该卷一定为可读写的。)如果目标路径不在挂载卷中并且此时的rootfs也是只读的,那么就返回一个错误

toVolume, err := checkIfPathIsInAVolume(container, absPath)
        if err != nil {
            return err
        }

        if !toVolume && container.HostConfig.ReadonlyRootfs {
            return errdefs.InvalidParameter(errors.New("container rootfs is marked read-only"))
        }

func checkIfPathIsInAVolume(container *container.Container, absPath string) (bool, error) {
    var toVolume bool
    parser := volumemounts.NewParser()
    for _, mnt := range container.MountPoints {
        if toVolume = parser.HasResource(mnt, absPath); toVolume {
            if mnt.RW {
                break
            }
            return false, errdefs.InvalidParameter(errors.New("mounted volume is marked read-only"))
        }
    }
    return toVolume, nil
}

解包的下一步操作是根据是否需要复制文件的UID和GID设置不同的options,并将options作为参数与打包文件的信息content和目标文件路径absPath共同传入archive.Untar函数执行具体的解包逻辑。

options := daemon.defaultTarCopyOptions(noOverwriteDirNonDir)

        if copyUIDGID {
            var err error
            // tarCopyOptions will appropriately pull in the right uid/gid for the
            // user/group and will set the options.
            options, err = daemon.tarCopyOptions(container, noOverwriteDirNonDir)
            if err != nil {
                return err
            }
        }

        return archive.Untar(content, absPath, options)



func (daemon *Daemon) tarCopyOptions(container *container.Container, noOverwriteDirNonDir bool) (*archive.TarOptions, error) {
    if container.Config.User == "" {
        return daemon.defaultTarCopyOptions(noOverwriteDirNonDir), nil
    }

    user, err := idtools.LookupUser(container.Config.User)
    if err != nil {
        return nil, err
    }

    identity := idtools.Identity{UID: user.Uid, GID: user.Gid}

    return &archive.TarOptions{
        NoOverwriteDirNonDir: noOverwriteDirNonDir,
        ChownOpts:            &identity,
    }, nil
}

如果copyUIDGID为false,那么options则为默认的配置。否则options由tarCopyOptions函数生成,该函数生成的options中会附带UID GID的信息。

GoInFS&&RunInFS

在上面的描述中我们会发现docker daemon使用了GoInFS和RunInFS函数将操作限制在了容器的上下文里,那么这是如何实现的呢?以GoInFS函数为例:

func (vw *containerFSView) GoInFS(ctx context.Context, fn func()) error {
    select {
    case vw.todo <- future{fn: func() error { fn(); return nil }}:
        return nil
    case <-ctx.Done():
        return ctx.Err()
    }
}

可以发现该函数实际上是将参数中的fn函数构建了一个future的结构体传入了容器文件系统的todo的channel中,那么todo是什么时候被调用的呢?在调用GoInFS之前,我们需要先使用openContainerFS函数获取一个容器的文件系统对象。这个函数的内部具体实现如下:

func (daemon *Daemon) openContainerFS(container *container.Container) (_ *containerFSView, err error) {
    ...
    todo := make(chan future)
    ...
    err = unshare.Go(unix.CLONE_NEWNS,
        func() error {
            if err := mount.MakeRSlave("/"); err != nil {
                return err
            }
            for _, m := range mounts {
                dest, err := container.GetResourcePath(m.Destination)
                if err != nil {
                    return err
                }

                var stat os.FileInfo
                stat, err = os.Stat(m.Source)
                if err != nil {
                    return err
                }
                if err := fileutils.CreateIfNotExists(dest, stat.IsDir()); err != nil {
                    return err
                }

                bindMode := "rbind"
                if m.NonRecursive {
                    bindMode = "bind"
                }
                writeMode := "ro"
                if m.Writable {
                    writeMode = "rw"
                    if m.ReadOnlyNonRecursive {
                        return errors.New("options conflict: Writable && ReadOnlyNonRecursive")
                    }
                    if m.ReadOnlyForceRecursive {
                        return errors.New("options conflict: Writable && ReadOnlyForceRecursive")
                    }
                }
                if m.ReadOnlyNonRecursive && m.ReadOnlyForceRecursive {
                    return errors.New("options conflict: ReadOnlyNonRecursive && ReadOnlyForceRecursive")
                }

                opts := strings.Join([]string{bindMode, writeMode, "rprivate"}, ",")
                if err := mount.Mount(m.Source, dest, "", opts); err != nil {
                    return err
                }

                if !m.Writable && !m.ReadOnlyNonRecursive {
                    if err := makeMountRRO(dest); err != nil {
                        if m.ReadOnlyForceRecursive {
                            return err
                        } else {
                            log.G(context.TODO()).WithError(err).Debugf("Failed to make %q recursively read-only", dest)
                        }
                    }
                }
            }

            return mounttree.SwitchRoot(container.BaseFS)
        },
        func() {
            defer close(done)

            for it := range todo {
                err := it.fn()
                if it.res != nil {
                    it.res <- err
                }
            }

            // The thread will terminate when this goroutine returns, taking the
            // mount namespace and all the volume bind-mounts with it.
        },
    )
    ...
}

该函数向unshare.Go函数传递一个unix.CLONE_NEWNS参数和两个函数类型的参数,我们先看第一个函数类型的参数

func() error {
            if err := mount.MakeRSlave("/"); err != nil {
                return err
            }
            for _, m := range mounts {
                dest, err := container.GetResourcePath(m.Destination)
                if err != nil {
                    return err
                }

                var stat os.FileInfo
                stat, err = os.Stat(m.Source)
                if err != nil {
                    return err
                }
                if err := fileutils.CreateIfNotExists(dest, stat.IsDir()); err != nil {
                    return err
                }

                bindMode := "rbind"
                if m.NonRecursive {
                    bindMode = "bind"
                }
                writeMode := "ro"
                if m.Writable {
                    writeMode = "rw"
                    if m.ReadOnlyNonRecursive {
                        return errors.New("options conflict: Writable && ReadOnlyNonRecursive")
                    }
                    if m.ReadOnlyForceRecursive {
                        return errors.New("options conflict: Writable && ReadOnlyForceRecursive")
                    }
                }
                if m.ReadOnlyNonRecursive && m.ReadOnlyForceRecursive {
                    return errors.New("options conflict: ReadOnlyNonRecursive && ReadOnlyForceRecursive")
                }

            
                opts := strings.Join([]string{bindMode, writeMode, "rprivate"}, ",")
                if err := mount.Mount(m.Source, dest, "", opts); err != nil {
                    return err
                }

                if !m.Writable && !m.ReadOnlyNonRecursive {
                    if err := makeMountRRO(dest); err != nil {
                        if m.ReadOnlyForceRecursive {
                            return err
                        } else {
                            log.G(context.TODO()).WithError(err).Debugf("Failed to make %q recursively read-only", dest)
                        }
                    }
                }
            }

            return mounttree.SwitchRoot(container.BaseFS)
        },

该函数进行了一些挂载操作之后使用mounttree.SwitchRoot函数切换到容器的跟文件系统
再看第二个函数类型参数

func() {
            defer close(done)
            for it := range todo {
                err := it.fn()
                if it.res != nil {
                    it.res <- err
                }
            }

        },

该函数的主要作用就是获取todo channel中的函数进行执行。
最后我们再来看一下unshare.Go具体做了什么

func Go(flags int, setupfn func() error, fn func()) error {
    ...
    go func() {
        ...
        if err := unix.Unshare(flags); err != nil {
            started <- os.NewSyscallError("unshare", err)
            return
        }
        if setupfn != nil {
            if err := setupfn(); err != nil {
                started <- err
                return
            }
        }
        close(started)

        if fn != nil {
            fn()
        }
    }()

    return <-started
}

unshare.Go函数先是调用unshare的系统调用创建了一个新的命名空间,然后一次调用第一个函数类型参数和第二个函数类型参数。综上所述,我们可以大致了解docker daemon是如何将一个函数限制在容器文件系统上下文中的了:首先使用unshare系统调用创建一个新的命名空间,将容器的文件挂载进该命名空间,并切换到容器的文件系统中,之后再调用用户想要执行的函数,此时这个函数就被限制在了容器的文件系统上下文中。

总结

无论是从容器中向宿主机复制文件还是从宿主机向容器复制文件,docker daemon都只负责容器的文件处理部分,docker client都只负责宿主机的文件处理部分,二者通过api接口进行通信,并且docker client和docker daemon的操作是互逆的,即docker client负责打包时,docker daemon则负责解包。
简单总结一下docker cp的流程,以从容器向宿主机复制文件为例。当我们使用类似docker cp containerid:containerFile hostDir的命令从容器内部向宿主机复制文件时,此时命令中的docker 实际上是docker client。docker client接受到用户输入的参数后判断出源文件路径和目标文件路径以及传输类型,之后根据传输类型进行不同的处理逻辑。在此例子中会进入到copyToContainer函数进行处理,此时docker client负责将源文件路径的文件进行打包。之后将打包好的文件信息以及容器信息,目标文件路径信息通过接口传递给docker daemon。docker daemon监听api接口发现有request时会根据api接口的不同进入不同的处理逻辑,此例子中,docker daemon会将传输过来的打包好的文件进行解包并将其移动到容器的目标文件路径下。当然这只是简单的描述一下整体的流程,这其中肯定会涉及到很多细节,如链接的处理,容器文件系统和宿主机文件系统的隔离,权限的判定等等。

你可能感兴趣的:(docker云原生安全)