docker cp 源码分析
Posted 石头的安全料理屋
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了docker cp 源码分析相关的知识,希望对你有一定的参考价值。
本文编写时,最新release为v20.10.6, 因此代码均为v20.10.6分支的代码
1. docker cp简介
在容器和宿主机的文件系统直接复制文件。执行cp命令的入口有两个,分别是docker container cp
和docker cp
, 两者作用相同, 后者可能在未来会被删除。
https://docs.docker.com/engine/reference/commandline/container_cp/
2. 源码入口位置
由cli接收cp命令参数,发送至docker engine api
cli与engine api的代码入口分别位于:
https://github.com/docker/cli/blob/v20.10.6/cli/command/container/cp.go#L43
https://github.com/moby/moby/blob/v20.10.6/api/server/router/container/copy.go
3. docker-cli
执行cp命令的两个入口分别位于
https://github.com/docker/cli/blob/v20.10.6/cli/command/container/cmd.go#L20
func NewContainerCommand(dockerCli command.Cli) *cobra.Command {
...
cmd.AddCommand(
...
NewCopyCommand(dockerCli),
...
https://github.com/docker/cli/blob/v20.10.6/cli/command/commands/commands.go#L96
func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
...
cmd.AddCommand(
...
hide(container.NewCopyCommand(dockerCli)),
...
两者分别用docker cp命令和docker container cp命令,两者都调用同一个函数NewCopyCommand
。
https://github.com/docker/cli/blob/v20.10.6/cli/command/container/cp.go#L43
func NewCopyCommand(dockerCli command.Cli) *cobra.Command {
var opts copyOptions
cmd := &cobra.Command{
...
Args: cli.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
if args[0] == "" {
return errors.New("source can not be empty")
}
if args[1] == "" {
return errors.New("destination can not be empty")
}
opts.source = args[0]
opts.destination = args[1]
return runCopy(dockerCli, opts)
},
}
flags := cmd.Flags()
flags.BoolVarP(&opts.followLink, "follow-link", "L", false, "Always follow symbol link in SRC_PATH")
flags.BoolVarP(&opts.copyUIDGID, "archive", "a", false, "Archive mode (copy all uid/gid information)")
return cmd
}
该函数定义了cp命令的参数, 具体实现调用runCopy
函数。仅支持从容器复制到宿主机、从宿主机复制到容器,分别调用copyFromContainer
,copyToContainer
,不支持容器间复制,也不支持仅在宿主机间复制。
https://github.com/docker/cli/blob/v20.10.6/cli/command/container/cp.go#L77
func runCopy(dockerCli command.Cli, opts copyOptions) error {
srcContainer, srcPath := splitCpArg(opts.source)
destContainer, destPath := splitCpArg(opts.destination)
...
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")
copyFromContainer
和copyToContainer
主要负责拼装参数,并调用docker engine api
3.1 copyFromContainer
https://github.com/docker/cli/blob/v20.10.6/cli/command/container/cp.go#L137-L154
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
...
srcPath = linkTarget
}
}
实际复制的操作是由CopyFromContainer函数通过调用api实现的。
https://github.com/docker/cli/blob/v20.10.6/cli/command/container/cp.go#L156
content, stat, err := client.CopyFromContainer(ctx, copyConfig.container, srcPath)
下面将调用api得到的输出复制到dstPath。
如果dstPath是-
, 就会直接输出到stdout。
https://github.com/docker/cli/blob/v20.10.6/cli/command/container/cp.go#L162-L179
if dstPath == "-" {
_, err = io.Copy(dockerCli.Out(), content)
return err
}
如果之前使用了followLink选项,因为更改了srcPath, 所以要把打包文件中的路径修正回原始的路径
preArchive := content
if len(srcInfo.RebaseName) != 0 {
_, srcBase := archive.SplitPathDirEntry(srcInfo.Path)
preArchive = archive.RebaseArchiveEntries(content, srcBase, srcInfo.RebaseName)
}
最后将打包文件复制到目的路径。
return archive.CopyTo(preArchive, srcInfo, dstPath)
这一过程虽然调用的是docker engine的pkg代码,但完全由cli执行, 主要就是将archive解压到dstPath。
https://github.com/moby/moby/blob/v20.10.6/pkg/archive/copy.go#L402
3.2 copyToContainer
https://github.com/docker/cli/blob/v20.10.6/cli/command/container/cp.go#L200-L219
dstStat, err := client.ContainerStatPath(ctx, copyConfig.container, dstPath)
// If the destination is a symbolic link, we should evaluate it.
if err == nil && dstStat.Mode&os.ModeSymlink != 0 {
linkTarget := dstStat.LinkTarget
if !system.IsAbs(linkTarget) {
// Join with the parent directory.
dstParent, _ := archive.SplitPathDirEntry(dstPath)
linkTarget = filepath.Join(dstParent, linkTarget)
}
dstInfo.Path = linkTarget
...
}
如果srcPath
是-
, 则目的路径必须是目录(根据帮助文档,设计上就是这样从stdin解压到目录,但不清楚为什么这么设计)
https://github.com/docker/cli/blob/v20.10.6/cli/command/container/cp.go#L236-L242
if srcPath == "-" {
content = os.Stdin
resolvedDstPath = dstInfo.Path
if !dstInfo.IsDir {
return errors.Errorf("destination \"%s:%s\" must be a directory", copyConfig.container, dstPath)
}
}
// Prepare source copy info.
srcInfo, err := archive.CopyInfoSourcePath(srcPath, copyConfig.followLink)
if err != nil {
return err
}
srcArchive, err := archive.TarResource(srcInfo)
if err != nil {
return err
}
defer srcArchive.Close()
dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, dstInfo)
if err != nil {
return err
}
defer preparedArchive.Close()
resolvedDstPath = dstDir
content = preparedArchive
最后复制到容器,实际复制的操作是由CopyToContainer函数通过调用api实现的。
options := types.CopyToContainerOptions{
AllowOverwriteDirWithFile: false,
CopyUIDGID: copyConfig.copyUIDGID,
}
return client.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options)
4. docker engine api
在cli中调用的api有3个:
ContainerStatPath
CopyFromContainer
CopyToContainer
对应的服务端代码分别为:
head
/containers/{name:.*}/archive
headContainersArchiveget
/containers/{name:.*}/archive
getContainersArchiveput
/containers/{name:.*}/archive
putContainersArchive
下面分析这三个函数的具体流程
4.1 headContainersArchive
(ContainerStatPath)
https://github.com/moby/moby/blob/v20.10.6/api/server/router/container/copy.go#L78
这个api有两个参数,分别是requestUri中传入的name(=ContainerID)和query中传入的path。
解析完name, path参数后,由ContainerStatPath函数处理
https://github.com/moby/moby/blob/v20.10.6/daemon/archive.go#L75
func (daemon *Daemon) ContainerStatPath(name string, path string) (stat *types.ContainerPathStat, err error) {
container, err := daemon.GetContainer(name)
...
stat, err = daemon.containerStatPath(container, path)
...
}
先挂载容器的文件系统和各个存储卷, 然后得到path在rootfs下的resolvedPath和absPath, 再根据它们得到ContainerPathStat
https://github.com/moby/moby/blob/v20.10.6/daemon/archive.go#L152
func (daemon *Daemon) containerStatPath(container *container.Container, path string) (stat *types.ContainerPathStat, err error) {
...
if err = daemon.Mount(container); err != nil {
return nil, err
}
...
err = daemon.mountVolumes(container)
...
resolvedPath, absPath, err := container.ResolvePath(path)
...
return container.StatPath(resolvedPath, absPath)
}
ContainerPathStat是这样得到的
https://github.com/moby/moby/blob/v20.10.6/container/archive.go#L51
func (container *Container) StatPath(resolvedPath, absPath string) (stat *types.ContainerPathStat, err error) {
...
lstat, err := driver.Lstat(resolvedPath)
if err != nil {
return nil, err
}
var linkTarget string
if lstat.Mode()&os.ModeSymlink != 0 {
// Fully evaluate the symlink in the scope of the container rootfs.
hostPath, err := container.GetResourcePath(absPath)
if err != nil {
return nil, err
}
linkTarget, err = driver.Rel(driver.Path(), hostPath)
if err != nil {
return nil, err
}
// Make it an absolute path.
linkTarget = driver.Join(string(driver.Separator()), linkTarget)
}
return &types.ContainerPathStat{
Name: driver.Base(absPath),
Size: lstat.Size(),
Mode: lstat.Mode(),
Mtime: lstat.ModTime(),
LinkTarget: linkTarget,
}, nil
}
最后,将ContainerPathStat返回到header头中
https://github.com/moby/moby/blob/v20.10.6/api/server/router/container/copy.go#L64
func setContainerPathStatHeader(stat *types.ContainerPathStat, header http.Header) error {
statJSON, err := json.Marshal(stat)
if err != nil {
return err
}
header.Set(
"X-Docker-Container-Path-Stat",
base64.StdEncoding.EncodeToString(statJSON),
)
return nil
}
4.2 getContainersArchive
(CopyFromContainer)
这个api有两个参数,分别是requestUri中传入的name(=ContainerID)和query中传入的path(=srcPath)。
打包完后将数据压缩并返回在body中。
https://github.com/moby/moby/blob/v20.10.6/api/server/router/container/copy.go#L115
func (s *containerRouter) getContainersArchive(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
v, err := httputils.ArchiveFormValues(r, vars)
if err != nil {
return err
}
tarArchive, stat, err := s.backend.ContainerArchivePath(v.Name, v.Path)
if err != nil {
return err
}
defer tarArchive.Close()
if err := setContainerPathStatHeader(stat, w.Header()); err != nil {
return err
}
w.Header().Set("Content-Type", "application/x-tar")
return writeCompressedResponse(w, r, tarArchive)
}
调用daemon.containerArchivePath
函数打包
https://github.com/moby/moby/blob/v20.10.6/daemon/archive.go#L100
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)
...
containerArchivePath
函数会创建一个关于path的归档文件,并返回一个ContainerPathStat
其中获取ContainerPathStat的过程与headContainersArchive几乎一致
https://github.com/moby/moby/blob/v20.10.6/daemon/archive.go#L182-L218
func (daemon *Daemon) containerArchivePath(container *container.Container, path string) (content io.ReadCloser, stat *types.ContainerPathStat, err error) {
...
if err = daemon.Mount(container); err != nil {
return nil, nil, err
}
...
if err = daemon.mountVolumes(container); err != nil {
return nil, nil, err
}
// Normalize path before sending to rootfs
path = container.BaseFS.FromSlash(path)
resolvedPath, absPath, err := container.ResolvePath(path)
...
stat, err = container.StatPath(resolvedPath, absPath)
...
}
关键在于打包的过程:
https://github.com/moby/moby/blob/v20.10.6/daemon/archive.go#L231-L263
driver := container.BaseFS
...
opts := archive.TarResourceRebaseOpts(sourceBase, driver.Base(absPath))
data, err := archivePath(driver, sourceDir, opts, container.BaseFS.Path())
...
content = ioutils.NewReadCloserWrapper(data, func() error {
err := data.Close()
container.DetachAndUnmount(daemon.LogVolumeEvent)
daemon.Unmount(container)
container.Unlock()
return err
})
如果driver(container.BaseFS)有实现ArchivePath函数,则优先使用该函数。
目前localfs均未实现该函数,仅发现remotefs(lcowfs)有此实现 https://github.com/moby/moby/blob/v20.10.6/daemon/graphdriver/lcow/remotefs.go#L83
否则,使用chrootarchive.Tar
打包
func archivePath(i interface{}, src string, opts *archive.TarOptions, root string) (io.ReadCloser, error) {
if ap, ok := i.(archiver); ok {
return ap.ArchivePath(src, opts)
}
return chrootarchive.Tar(src, opts, root)
}
4.2.1 chrootarchive.Tar
https://github.com/moby/moby/blob/v20.10.6/pkg/chrootarchive/archive.go#L101
func Tar(srcPath string, options *archive.TarOptions, root string) (io.ReadCloser, error) {
if options == nil {
options = &archive.TarOptions{}
}
return invokePack(srcPath, options, root)
}
调用docker-tar
命令打包
https://github.com/moby/moby/blob/v20.10.6/pkg/chrootarchive/archive_unix.go#L178
func invokePack(srcPath string, options *archive.TarOptions, root string) (io.ReadCloser, error) {
if root == "" {
return nil, errors.New("root path must not be empty")
}
...
cmd := reexec.Command("docker-tar", relSrc, root)
docker-tar的实现对应tar函数
https://github.com/moby/moby/blob/v20.10.6/pkg/chrootarchive/init_unix.go#L17
reexec.Register("docker-tar", tar)
具体实现调用archive.TarWithOptions
(关于archive模块的源码分析,暂不在本文展开), 但在此之前先进行了chroot
func tar() {
...
if err := realChroot(root); err != nil {
fatal(err)
}
...
rdr, err := archive.TarWithOptions(src, &options)
if err != nil {
fatal(err)
}
defer rdr.Close()
if _, err := io.Copy(os.Stdout, rdr); err != nil {
fatal(err)
}
os.Exit(0)
}
4.2.2 lcowfs
lcowfs通过执行remotefs archivepath path
命令打包
https://github.com/moby/moby/blob/v20.10.6/daemon/graphdriver/lcow/remotefs.go#L83
if err := l.runRemoteFSProcess(tarBuf, w, remotefs.ArchivePathCmd, src); err != nil {
...
https://github.com/moby/moby/blob/v20.10.6/daemon/graphdriver/lcow/remotefs.go#L127-L129
func (l *lcowfs) runRemoteFSProcess(stdin io.Reader, stdout io.Writer, args ...string) error {
...
cmd := fmt.Sprintf("%s %s", remotefs.RemotefsCmd, strings.Join(args, " "))
stderr := &bytes.Buffer{}
if err := l.currentSVM.runProcess(cmd, stdin, stdout, stderr); err != nil {
return err
}
...
}
remotefs
是一个大命令,archivepath
是其中一个子命令。archivePath
对应的实现为
https://github.com/moby/moby/blob/v20.10.6/vendor/github.com/Microsoft/opengcs/service/gcsutils/remotefs/remotefs.go#L48
// Commands provide a string -> remotefs function mapping.
// This is useful for commandline programs that will receive a string
// as the function to execute.
var Commands = map[string]Func{
...
ArchivePathCmd: ArchivePath,
}
archivepath具体实现如下,打包函数实际为archive.TarWithOptions, 关于archive模块的源码分析,暂不在本文展开。
https://github.com/moby/moby/blob/v20.10.6/vendor/github.com/Microsoft/opengcs/service/gcsutils/remotefs/remotefs.go#L559
// ArchivePath archives the given directory and writes it to out.
// Args:
// - in = size of json | json of archive.TarOptions
// - args[0] = source directory name
// Out:
// - out = tar file of the archive
func ArchivePath(in io.Reader, out io.Writer, args []string) error {
if len(args) < 1 {
return ErrInvalid
}
opts, err := ReadTarOptions(in)
if err != nil {
return err
}
r, err := archive.TarWithOptions(args[0], opts)
if err != nil {
return err
}
if _, err := io.Copy(out, r); err != nil {
return err
}
return nil
}
4.3 putContainersArchive
(CopyToContainer)
putContainersArchive
有4个由客户端传入的参数,分别是requestUri中传入的name(=ContainerID),query中传入的path(=dstPath), noOverwriteDirNonDir(=true), copyUIDGID(=true)
https://github.com/moby/moby/blob/v20.10.6/client/container_copy.go#L33-L59
query.Set("path", filepath.ToSlash(dstPath)) // Normalize the paths used in the API.
// Do not allow for an existing directory to be overwritten by a non-directory and vice versa.
if !options.AllowOverwriteDirWithFile {
query.Set("noOverwriteDirNonDir", "true")
}
if options.CopyUIDGID {
query.Set("copyUIDGID", "true")
}
apiPath := "/containers/" + containerID + "/archive"
解析完参数后,由putContainersArchive
函数解包
https://github.com/moby/moby/blob/v20.10.6/api/server/router/container/copy.go#L135
func (s *containerRouter) putContainersArchive(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
...
return s.backend.ContainerExtractToDir(v.Name, v.Path, copyUIDGID, noOverwriteDirNonDir, r.Body)
}
containerExtractToDir
函数中,一开始仍然是先执行mount
rootfs和volume操作
https://github.com/moby/moby/blob/v20.10.6/daemon/archive.go#L273-L289
func (daemon *Daemon) containerExtractToDir(container *container.Container, path string, copyUIDGID, noOverwriteDirNonDir bool, content io.Reader) (err error) {
container.Lock()
defer container.Unlock()
if err = daemon.Mount(container); err != nil {
return err
}
defer daemon.Unmount(container)
err = daemon.mountVolumes(container)
defer container.DetachAndUnmount(daemon.LogVolumeEvent)
if err != nil {
return err
}
// Normalize path before sending to rootfs'
path = container.BaseFS.FromSlash(path)
driver := container.BaseFS
获取软链接目的路径,判断目的路径是否是目录,如果不是目录,返回报错
https://github.com/moby/moby/blob/v20.10.6/daemon/archive.go#L315-L318
func (daemon *Daemon) containerExtractToDir(container *container.Container, path string, copyUIDGID, noOverwriteDirNonDir bool, content io.Reader) (err error) {
...
resolvedPath, err := container.GetResourcePath(absPath)
if err != nil {
return err
}
stat, err := driver.Lstat(resolvedPath)
if err != nil {
return err
}
if !stat.IsDir() {
return ErrExtractPointNotDirectory
}
限制目的路径不能是只读的
https://github.com/moby/moby/blob/v20.10.6/daemon/archive.go#L356-L363
func (daemon *Daemon) containerExtractToDir(container *container.Container, path string, copyUIDGID, noOverwriteDirNonDir bool, content io.Reader) (err error) {
...
toVolume, err := checkIfPathIsInAVolume(container, absPath)
...
if !toVolume && container.HostConfig.ReadonlyRootfs {
return ErrRootFSReadOnly
}
调用extractArchive
函数解包,如果配置了copyUIDGID,会把文件的uid,gid修改为容器内的主用户,否则保留原文件的uid,gid。
https://github.com/moby/moby/blob/v20.10.6/daemon/archive.go#L365-L379
func (daemon *Daemon) containerExtractToDir(container *container.Container, path string, copyUIDGID, noOverwriteDirNonDir bool, content io.Reader) (err error) {
...
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
}
}
if err := extractArchive(driver, content, resolvedPath, options, container.BaseFS.Path()); err != nil {
return err
}
如果driver(container.BaseFS)有ExtractArchive方法,则使用该方法(目前只有lcowfs实现了该方法)。否则调用chrootarchive.UntarWithRoot
https://github.com/moby/moby/blob/v20.10.6/daemon/archive.go#L34
func extractArchive(i interface{}, src io.Reader, dst string, opts *archive.TarOptions, root string) error {
if ea, ok := i.(extractor); ok {
return ea.ExtractArchive(src, dst, opts)
}
return chrootarchive.UntarWithRoot(src, dst, opts, root)
}
4.3.1 chrootarchive.UntarWithRoot
https://github.com/moby/moby/blob/v20.10.6/pkg/chrootarchive/archive.go#L54
func UntarWithRoot(tarArchive io.Reader, dest string, options *archive.TarOptions, root string) error {
return untarHandler(tarArchive, dest, options, true, root)
}
如果容器内不存在目的目录,就创建并chown,然后调用invokeUnpack
函数。
https://github.com/moby/moby/blob/v20.10.6/pkg/chrootarchive/archive.go#L66
func untarHandler(tarArchive io.Reader, dest string, options *archive.TarOptions, decompress bool, root string) error {
...
if _, err := os.Stat(dest); os.IsNotExist(err) {
if err := idtools.MkdirAllAndChownNew(dest, 0755, rootIDs); err != nil {
return err
}
}
...
return invokeUnpack(r, dest, options, root)
}
调用docker-untar命令解包
https://github.com/moby/moby/blob/v20.10.6/pkg/chrootarchive/archive_unix.go#L61
func invokeUnpack(decompressedArchive io.Reader, dest string, options *archive.TarOptions, root string) error {
...
cmd := reexec.Command("docker-untar", dest, root)
cmd.Stdin = decompressedArchive
...
if err := cmd.Start(); err != nil {
w.Close()
return fmt.Errorf("Untar error on re-exec cmd: %v", err)
}
...
}
docker-untar实现对应chrootarchive.untar函数,先chroot,再调用archive.Unpack解包。关于archive模块的源码分析,暂不在本文展开。
https://github.com/moby/moby/blob/v20.10.6/pkg/chrootarchive/archive_unix.go#L25
func untar() {
...
if err := chroot(root); err != nil {
fatal(err)
}
if err := archive.Unpack(os.Stdin, dst, &options); err != nil {
fatal(err)
}
...
}
4.3.2 lcowfs
lcowfs中调用extractarchive
实现解包
https://github.com/moby/moby/blob/v20.10.6/daemon/graphdriver/lcow/remotefs.go#L77
func (l *lcowfs) ExtractArchive(src io.Reader, dst string, opts *archive.TarOptions) error {
...
if err := l.runRemoteFSProcess(input, nil, remotefs.ExtractArchiveCmd, dst); err != nil {
}
extractarchive
命令对应ExtractArchive
函数
https://github.com/moby/moby/blob/v20.10.6/vendor/github.com/Microsoft/opengcs/service/gcsutils/remotefs/remotefs.go#L47
var Commands = map[string]Func{
...
ExtractArchiveCmd: ExtractArchive,
ExtractArchive
函数中调用archive.Untar
func ExtractArchive(in io.Reader, out io.Writer, args []string) error {
...
if err := archive.Untar(in, args[0], opts); err != nil {
}
以上是关于docker cp 源码分析的主要内容,如果未能解决你的问题,请参考以下文章
Android 插件化VirtualApp 源码分析 ( 目前的 API 现状 | 安装应用源码分析 | 安装按钮执行的操作 | 返回到 HomeActivity 执行的操作 )(代码片段
Android 逆向整体加固脱壳 ( DEX 优化流程分析 | DexPrepare.cpp 中 dvmOptimizeDexFile() 方法分析 | /bin/dexopt 源码分析 )(代码片段
Android 事件分发事件分发源码分析 ( Activity 中各层级的事件传递 | Activity -> PhoneWindow -> DecorView -> ViewGroup )(代码片段