使用golang理解Linux namespace(三)- User

上一篇讲到,我们同时使用了不同命名空间,包括 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之前

[email protected]:~$ 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

[email protected]:~$ ./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匹配问题 如果还不清楚的直接参考下面图片:

idmap 如图所示,namespace1 的进程C,通过clone()创建了一个进程D,且D拥有了新的命名空间2,由于进程C是在UID1000,不是root用户。对于D进程而言,它在新的命名空间namespace2下确实是具备root用户性质的,但是从namespace1来看,它却是非root用户,同C进程。因此进程D只有root其名而无其权限

这时候我们需要建立一种映射。

撸起袖子

所幸的是,go已经考虑到这种情况,cmd.SysProcAttr有两个属性UidMappingsGidMappings,两者都表示类型SysProcIDMap,他的定义长这样子:

type SysProcIDMap struct {
    ContainerID int // Container ID.
    HostID      int // Host ID.
    Size        int // Size.
}

ContainerID 和 HostID根据名字就能猜到是什么id,至于size,文档表示要映射的ID范围,貌似这个是允许一次映射多个id的,所以是有个范围值,暂时用1对1比较稳妥。//todo

添加UidMappingsGidMappings后,现在代码应该是这样的:

// +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下测试:

[email protected]:~/workspace/gons$ go build ns-proc.go
ub[email protected]:~/workspace/gons$ whoami
ubuntu
[email protected]:~/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