容器:namespace文件隔离实现 最近很开心,准备写一个docker系列的读书笔记,记录我在腾讯云逗比的青春。 参考资料: 自己动手写docker-4 https://juejin.im/post/5c2b495af265da6134388142 使用golang理解Linux namespace(四)-clone前的初始化 https://here2say.com/38/ 代码大部分来源于上面两篇文章。 ## 引言 首先偷一张图:  容器的实现,依赖于Linux下的命名空间隔离,基础知识就不说啦。 ## 文件隔离 第一篇准备从容器的文件隔离读起,因为文件系统的配置比较麻烦。 ### 创建第一个程序,实现clone 在go里实现clone,docker使用了一个比较巧妙的方法,在linux中,/proc/self/exe 文件代表自己,也就是说,在程序中cmd请求这个地址,等同于使用自己再启动一个实例,也就是clone。 docker的库官方实现了这个功能,go包的地址是:"github.com/docker/docker/pkg/reexec" 也就是说,可以使用下面的代码来实现clone的功能 ``` package main import ( "fmt" "github.com/docker/docker/pkg/reexec" "main/code/dockertest" "os" "os/exec" "syscall" ) func init() { fmt.Printf("arg0=%s,\n", os.Args[0]) reexec.Register("initFuncName", func() { fmt.Printf("\n>> namespace setup code goes here <<\n\n") newRoot := os.Args[1] fmt.Printf("newRoot:%s\n", newRoot) ////在这里写子程序的代码 }) if reexec.Init() { os.Exit(0) } } func main() { var rootfsPath = "/tmp/ns-proc/rootfs" cmd := reexec.Command("initFuncName", rootfsPath) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Printf("Error running the reexec.Command - %s\n", err) os.Exit(1) } } ``` ### 隔离文件系统 当调用clone函数,传入CLONE_NEWNS的时候,会惊奇的发现,容器仍然可以访问主机上的所有目录。这是因为新命名空间会把原调用者的mount list直接复制。 要解决这个问题,需要在使用命名空间隔离之前调用pivot_root系统调用将rootfs切换,go已经在syscall里面封装好了,下面操作: - 下载一个需要的rootfs,比如alpine-minirootfs-3.9.3-x86_64.tar.gz。谷歌一下就好。 - mkdir -p /tmp/ns-proc/rootfs - tar -C /tmp/ns-proc/rootfs/ -xf alpine-minirootfs-3.9.3-x86_64.tar.gz - 封装一下pivot的调用: ``` package dockertest import ( "fmt" "os" "path/filepath" "syscall" ) //implement pivot_root by syscall func PivotRoot(newroot string) error { preRoot := "/.pivot_root" putold := filepath.Join(newroot, preRoot) //putold:/tmp/ns-proc/rootfs/.pivot_root fmt.Printf("root:%v, old:%v\n", newroot, putold) // pivot_root requirement that newroot and putold must not be on the same filesystem as the current root //current root is / and new root is /tmp/ns-proc/rootfs and putold is /tmp/ns-proc/rootfs/.pivot_root //thus we bind mount newroot to itself to make it different //try to comment here you can see the error if err := syscall.Mount(newroot, newroot, "", syscall.MS_BIND|syscall.MS_REC, ""); err != nil { fmt.Println("mount error", err) return err } // create putold directory, equal to mkdir -p xxx if err := os.MkdirAll(putold, 0700); err != nil { fmt.Println("mkdir error", err) return err } // call pivot_root fmt.Printf("pivot newroot:%v, putold:%v\n", newroot, putold) if err := syscall.PivotRoot(newroot, putold); err != nil { fmt.Println("PivotRoot error", err) return err } // ensure current working directory is set to new root if err := os.Chdir("/"); err != nil { fmt.Println("chdir error", err) return err } // umount putold, which now lives at /.pivot_root putold = preRoot if err := syscall.Unmount(putold, syscall.MNT_DETACH); err != nil { return err } // remove putold if err := os.RemoveAll(putold); err != nil { return err } return nil } ``` - 在main里面进行调用,依照第一节clone的方法,可以把子程序改成用cmd启动`sh`。 - 可以参考下面的代码,`nsRun`就使用了系统的namespace在cmd启动的时候进行了隔离。 ``` package main import ( "fmt" "github.com/docker/docker/pkg/reexec" "main/code/dockertest" "os" "os/exec" "syscall" ) func init() { fmt.Printf("arg0=%s,\n", os.Args[0]) reexec.Register("initFuncName", func() { fmt.Printf("\n>> namespace setup code goes here <<\n\n") newRoot := os.Args[1] fmt.Printf("newRoot:%s\n", newRoot) if err := dockertest.PivotRoot(newRoot); err != nil { fmt.Printf("Error running pivot_root - %s\n", err) os.Exit(1) } nsRun() //calling clone() to create new process goes here }) if reexec.Init() { os.Exit(0) } } func nsRun() { cmd := exec.Command("sh") cmd.Dir = "/" //set identify for this demo cmd.Env = []string{"PS1=-[namespace-process]-# "} cmd.SysProcAttr = &syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWNS, } cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Printf("Error running the /bin/sh command - %s\n", err) os.Exit(1) } } func main() { var rootfsPath = "/tmp/ns-proc/rootfs" cmd := reexec.Command("initFuncName", rootfsPath) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Printf("Error running the reexec.Command - %s\n", err) os.Exit(1) } } ``` - 编译启动,运行程序`./main`,但是,此时开始报错:`pivot_root invalid argument` - 搜索了一下,发现是`You may have some mounts with MS_SHARED`。具体参考:[https://bugzilla.redhat.com/show_bug.cgi?id=1361043](https://bugzilla.redhat.com/show_bug.cgi?id=1361043) - 按上面的解决, ``` go build main sudo unshare -m # 此时会进入到另一个以root为用户的账户下 ./main ``` 大功告成,可以`cd /`,看看是不是文件系统已经换了。示意图:  来自 大脸猪 写于 2020-01-16 21:49 -- 更新于2020-10-19 13:06 -- 0 条评论