此篇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则需要根据如下两种情况讨论:
- 如果打开uid_map/gid_map的进程和目标进程在同一个namespace,则这里的ID_outside_ns应该为parent USER namespace中的一个uid/gid。这种情况常见于child自己设置映射。
- 如果打开uid_map/gid_map的进程和目标进程不在同一个namespace,则这里的ID_outside_ns则为该进程所在USER namespace中的一个uid/gid。最常见的就是parent为child设置映射。
除了出了上述的格式要求,对于uid/gid的映射还有几点约束:
- 写入uid_map/gid_map的进程,必须对PID进程所属USER namespace拥有CAP_SETUID/CAP_SETGID权限。
- 写入uid_map/gid_map的进程,必须位于PID进程的parent或者child USER namespace。
满足如下条件之一:
- 写入的数据将写入进程在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技术,想要深入地了解,还需要大家下功夫钻研。谢谢阅读!
纠正一下,在ArchWiki中Linux Container一文中有提到: Archlinux的内核是开启了CONFIG_USER_NS的,只是出于安全考虑,仅限于root用户使用