上一篇讲到,我们同时使用了不同命名空间,包括 Mount, UTS, IPC, PID, Network还有User命名空间。然而,其实还遗留下很多东西,比如缺少初始化,配置等操作。这次会详细讲解其中的问题。
回顾上篇代码
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags:
syscall.CLONE_NEWNS |
syscall.CLONE_NEWUTS |
syscall.CLONE_NEWIPC |
syscall.CLONE_NEWPID |
syscall.CLONE_NEWNET |
syscall.CLONE_NEWUSER,
}
在上篇,代码添加了User这个命名空间到ns-proc这个程序中,然后我们再运行这个ns-proc程序就不需要添加sudo就可以运行了,换言之,ns-proc不必用root用户就可以启动,并且是具备root用户权限的。这就是它神奇的地方,不用sudo也能变成root运行。
是不是真的这样??
理论上是的,因为添加了User这个namespac,脱离原来的用户空间,默认新的进程就会用root
以下代码是在添加syscall.CLONE_NEWUSER这个flag之前
ubuntu@VM-0-7-ubuntu:~$ sudo ./ns-proc
-[namespace-process]-# whoami
root
-[namespace-process]-# id root
uid=0(root) gid=0(root) groups=0(root)
这个是添加了syscall.CLONE_NEWUSER这个flag之后build出来的ns-proc
ubuntu@VM-0-7-ubuntu:~$ ./ns-proc
-[namespace-process]-# whoami
nobody
-[namespace-process]-# id nobody
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)
-[namespace-process]-#
额,这下有点尴尬😅
以非root用户运行,但是在这新命令空间下的sh进程执行whoami指令发现结果并不是root用户,用id命令对比下可以看到uid和gid
那么,这算是我们使用namespace搞出来的第一个bug,本文就是要解决这个问题,新的namespace下丢失root信息。
UID和GID的映射
我们ns-proc显示不是root用户这个问题,其实就是因为配置不当,导致我们丢了root用户的一些信息。因此里面的进程并没有如期显示root用户,也就是说仅仅添加了CLONE_NEWUSER
这个flag是不够的,它确实能在新用户空间下。
要搞懂这个问题,涉及一个UID和GID的映射。 id映射问题要讲起来也挺多,直接讲重点:
- User这个命名空间提供了UID和GID的分离环境。
- 在一个host主机上,可以同时有一个或者多个不同的User 命名空间在被使用
- 每一个Linux进程都在其中一个User命名空间下执行
- User命名空间允许这样的情况:User namespace A下的进程p使用的UID和该进程在 User namespace B使用不同的UID
- UID和GID映射解决上面这个不同User namespaces下的ID匹配问题 如果还不清楚的直接参考下面图片:
如图所示,namespace1 的进程C,通过clone()创建了一个进程D,且D拥有了新的命名空间2,由于进程C是在UID1000,不是root用户。对于D进程而言,它在新的命名空间namespace2下确实是具备root用户性质的,但是从namespace1来看,它却是非root用户,同C进程。因此进程D只有root其名而无其权限
这时候我们需要建立一种映射。
撸起袖子
所幸的是,go已经考虑到这种情况,cmd.SysProcAttr
有两个属性UidMappings
和GidMappings
,两者都表示类型SysProcIDMap,他的定义长这样子:
type SysProcIDMap struct {
ContainerID int // Container ID.
HostID int // Host ID.
Size int // Size.
}
ContainerID 和 HostID根据名字就能猜到是什么id,至于size,文档表示要映射的ID范围,貌似这个是允许一次映射多个id的,所以是有个范围值,暂时用1对1比较稳妥。//todo
添加UidMappings
和GidMappings
后,现在代码应该是这样的:
// +build linux
package main
import (
"fmt"
"os"
"os/exec"
"syscall"
)
func main() {
cmd := exec.Command("sh")
//set identify for this demo
cmd.Env = []string{"PS1=-[namespace-process]-# "}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWNS |
syscall.CLONE_NEWUTS |
syscall.CLONE_NEWIPC |
syscall.CLONE_NEWPID |
syscall.CLONE_NEWNET |
syscall.CLONE_NEWUSER,
UidMappings: []syscall.SysProcIDMap{
{
ContainerID: 0,
HostID: os.Getuid(),
Size: 1,
},
},
GidMappings: []syscall.SysProcIDMap{
{
ContainerID: 0,
HostID: os.Getgid(),
Size: 1,
},
},
}
if err := cmd.Run(); err != nil {
fmt.Printf("Error running the /bin/sh command - %s\n", err)
os.Exit(1)
}
}
可以看到,分别添加了UidMappings和GidMappings两个参数, 并将当前用户uid和gid分别映射为容器内的id,
host.uid---container_uid:0
host.gid---container_gid:0
由os.Getuid()这个用户id创建的container(不同命名空间下的新进程)的uid和gid都是0,也就是说设置了在root用户下。
重新编译下,把ns-proc在Ubuntu下测试:
ubuntu@VM-0-7-ubuntu:~/workspace/gons$ go build ns-proc.go
ubuntu@VM-0-7-ubuntu:~/workspace/gons$ whoami
ubuntu
ubuntu@VM-0-7-ubuntu:~/workspace/gons$ ./ns-proc
-[namespace-process]-# whoami
root
-[namespace-process]-# id
uid=0(root) gid=0(root) groups=0(root),65534(nogroup)
-[namespace-process]-#
这次我们没有使用root权限运行的ns-proc,可以看到实际上sh是在新的namespace下创建,且是在root用户下跑起来的,id=0。
可以看到docker容器也是这样的一种场景,我们通过UidMapping/GidMapping
使得在非root用户启动的ns-proc运行在新的命名空间下,且具备了root权限。
完整的工程代码可以访问git仓库获取,鉴于国内网络,暂时先托管国内gitee仓库
接下来做什么
本次主要解决了一个问题,介绍了容器uid和gid的映射,通过设置UidMapping/GidMapping
,配置id映射,解决容器进程丢失root权限。使得新创建的命名空间下的进程更像docker容器。
接下来,会继续讲解golang使用namespace相关的知识点reexec
。