Linux namespace 简介 part 6 - USER

此篇post为译者续写,非原作者文章翻译,特此声明。

继上一篇 [关于NET namespace的文章]1,此篇post,我们将为大家介绍这个系列的最后一个namespace——USER,通过这个namespace,我们将能够使得例子中的那个bash(如果你愿意称其为VM)更加透明。如果你尚未阅读过之前的post,我强烈建议你先阅读一遍这个系列的第一篇post,了解下linux namespace隔离机制。

首先,有一点需要特别说明,USER namespace是在Kernel-3.8才被(大部分)实现的,当然并不是说如果你的内核版本高于3.8就一定支持,很多发行版因为安全问题,在编译内核的时候并未开启USER_NS,比如Archlinux。因此,阅读此篇post之前,请确定你的系统支持USER_NS。下文所有的example都是在ubuntu14.04中运行的。

要激活USER namespace,只需要把“CLONE_NEWUSER”标记添加到“clone”调用。不需要其他额外的步骤。它也能和其他namespace组合使用。有一点和其他的namespace不同,创建USER namespace不需要特权,我们先来看一个例子:

#define _GNU_SOURCE
#include <sys/capability.h>
#include <sys/wait.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define STACK_SIZE (1024 * 1024)
/* Space for child's stack */
static char child_stack[STACK_SIZE];
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); } while (0)
int child_main(void *arg) {
    cap_t caps;
    for (;;) {
        printf("eUID = %ld;  eGID = %ld;  ",
                (long) geteuid(), (long) getegid());
        caps = cap_get_proc();
        /* Show the capabilities of child process */
        printf("capabilities: %s\n", cap_to_text(caps, NULL));
        if (arg == NULL) break;
        sleep(5);
    }
    return 0;
}
int main(int argc, char *argv[]) {
    /* Create child; child commences execution in child_main() */
    pid_t pid = clone(child_main, child_stack + STACK_SIZE, 
                CLONE_NEWUSER | SIGCHLD, argv[1]);
    /* If your kernel don't support USER_NS, it will fail */
    if (pid == -1) errExit("clone");
    /* Parent falls through to here.  Wait for child. */
    if (waitpid(pid, NULL, 0) == -1) errExit("waitpid");
    exit(EXIT_SUCCESS);
}

编译并运行上述代码:

codesun@codesun-desktop:~$ gcc user.c -Wall -o user -lcap && ./user
eUID = 65534;  eGID = 65534;  capabilities: = cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend+ep

由于我们并没有为child映射uid和gid,因此这里显示默认的65534,这个值在/proc/sys/kernel/overflowuid和/proc/sys/kernel/overflowgid中定义。同时,我们还看到了capabilities字段,自Kernel-2.2起,Linux将权限进行了拆分,每个权限单元都能够独立地开启或者关闭,更多内容请看Capabilities。上述示例中的相关内容就表示了新的USER namespace中,child拥有的权限。

那么,我们应该如何为新创建的USER namespace映射uid和gid呢?很简单,只需要写如下文件,其中的PID是child的PID:

  • /proc/PID/uid_map
  • /proc/PID/gid_map

这两个文件都遵循如下格式:

ID_inside_ns ID_outside_ns length

其中ID_inside_ns和length决定了映射的范围,而ID_outside_ns则需要根据如下两种情况讨论:

  1. 如果打开uid_map/gid_map的进程和目标进程在同一个namespace,则这里的ID_outside_ns应该为parent USER namespace中的一个uid/gid。这种情况常见于child自己设置映射。
  2. 如果打开uid_map/gid_map的进程和目标进程不在同一个namespace,则这里的ID_outside_ns则为该进程所在USER namespace中的一个uid/gid。最常见的就是parent为child设置映射。

除了出了上述的格式要求,对于uid/gid的映射还有几点约束:

  1. 写入uid_map/gid_map的进程,必须对PID进程所属USER namespace拥有CAP_SETUID/CAP_SETGID权限。
  2. 写入uid_map/gid_map的进程,必须位于PID进程的parent或者child USER namespace。
  3. 满足如下条件之一:

    • 写入的数据将写入进程在parent USER namespace中的有效uid/gid映射到child USER namespace。此条规则允许child USER namespace中的进程为自己设置uid/gid
    • 进程在parent USER namespace拥有CAP_SETUID/CAP_SETGID权限,那么它将可以映射到parent USER namespace中的任一uid/gid。不过由于child USER namespace中新创建的进程是没有在parent中的权限的,那么此条规则仅用于,位于具有相应权限的parent USER namespace中的进程,来映射同namespace内的任一IDs。

好吧,终于结束了恶心的规则,我们继续来补全之前几个post中的例子——一个完全隔离的bash。

#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/mount.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#define STACK_SIZE (1024 * 1024)
// sync primitive
int checkpoint[2];
static char child_stack[STACK_SIZE];
char* const child_args[] = {
  "/bin/bash",
  NULL
};
void set_uid_map(pid_t pid, int inside_id, int outside_id, int length) {
    char path[256];
    sprintf(path, "/proc/%d/uid_map", getpid());
    FILE* uid_map = fopen(path, "w");
    fprintf(uid_map, "%d %d %d", inside_id, outside_id, length);
    fclose(uid_map);
}
void set_gid_map(pid_t pid, int inside_id, int outside_id, int length) {
    char path[256];
    sprintf(path, "/proc/%d/gid_map", getpid());
    FILE* gid_map = fopen(path, "w");
    fprintf(gid_map, "%d %d %d", inside_id, outside_id, length);
    fclose(gid_map);
}
int child_main(void* arg) {
  char c;
  // init sync primitive
  close(checkpoint[1]);
  // setup hostname
  printf(" - [%5d] World !\n", getpid());
  sethostname("In Namespace", 12);
  // remount "/proc" to get accurate "top" && "ps" output
  mount("proc", "/proc", "proc", 0, NULL);
  // wait for network setup in parent
  read(checkpoint[0], &c, 1);
  set_uid_map(getpid(), 0, 0, 1);
  set_gid_map(getpid(), 0, 0, 1);
  // setup network
  system("ip link set lo up");
  system("ip link set veth1 up");
  system("ip addr add 169.254.1.2/30 dev veth1");
  execv(child_args[0], child_args);
  printf("Ooops\n");
  return 1;
}
int main() {
  // init sync primitive
  pipe(checkpoint);
  printf(" - [%5d] Hello ?\n", getpid());
  int child_pid = clone(child_main, child_stack+STACK_SIZE,
      CLONE_NEWUSER | CLONE_NEWUSER | CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWNET | SIGCHLD, NULL);
  // further init: create a veth pair
  char* cmd;
  asprintf(&cmd, "ip link set veth1 netns %d", child_pid);
  system("ip link add veth0 type veth peer name veth1");
  system(cmd);
  system("ip link set veth0 up");
  system("ip addr add 169.254.1.1/30 dev veth0");
  free(cmd);
  // signal "done"
  close(checkpoint[1]);
  waitpid(child_pid, NULL, 0);
  return 0;
}

OK,似乎很不错的样子,这里有一点需要注意,由于我们是在child中设置uid/gid的映射,因此需要遵循规则3中的要求进行映射,即映射到root。

等等,为何是root?不是说开启USER namespace是不需要root权限的吗?我可没撒谎,这里需要root权限,是因为USER namespace和其他需要特权的namespace混用了。当我得到这个结论后,感到很诧异,也许大家也会有这种感觉,USER的用处似乎不大,事实并不是这样的,只不过现在USER namespace并不成熟,很多发行版甚至是直接不支持。笔者认为只要默认CAP设置得好,支持USER namespace并不会造成很大的困扰。

那么我们如何才能在这个例子中真正发挥USER namespace的威力呢?可以试着这么做,先以USER namespace创建一个子进程,将UID映射到0,此时我们就是root了,并且在ubuntu14.04中,CAP也是能够支持后续操作的。然后,再继续创建child bash,将剩下的namespace添加上,此时这个例子就像那么一回事了。这个就留给大家尝试一下。

还等什么,Run it!

codesun@codesun-desktop:~$ gcc user.c -o user -Wall -lcap && sudo ./user
 - [ 4327] Hello ?
 - [    1] World !
root@In Namespace:~# nc -l 4242
hello
world

当然,为了上述nc命令能够正确执行,请在另外一个终端内执行如下命令:

codesun@codesun-desktop:~$ nc 169.254.1.2 4242
hello
world

至此,我们已经拥有了一个完全隔离的bash,你也可以称之为“容器”,是不是很简单?当然这个系列只能够引导大家入门,一览Linux的namespace技术,想要深入地了解,还需要大家下功夫钻研。谢谢阅读!

仅有一条评论
  1. Peven Peven

    纠正一下,在ArchWiki中Linux Container一文中有提到: Archlinux的内核是开启了CONFIG_USER_NS的,只是出于安全考虑,仅限于root用户使用

说两句: