1. User Namespace
User Namespace 可以让容器内部看到的 UID 和 GID 和外部是不同的了,它可以隔离了一台 Linux 节点上的 User ID(uid)和 Group ID(gid),它给 Namespace 中的 uid/gid 的值与宿主机上的 uid/gid 值建立了一个映射关系。经过 User Namespace 的隔离,我们在 User Namespace 中看到的进程的 uid/gid,就和宿主机 Namespace 中看到的 uid 和 gid 不一样了。比如容器内部针对 dawn 这个用户显示的是 0,但是实际上这个用户在主机上应该是 1000。
如下图所示,namespace_1 里的 uid 值是 0 到 999,但其实它在宿主机上对应的 uid 值是 1000 到 1999。另外,User Namespace 是可以嵌套的,比如下面图里的 namespace_2 里可以再建立一个 namespace_3,这个嵌套的特性是其他 Namespace 没有的。
"Linux Programmer's Manual"中关于 User Namespace 的阐述,https://man7.org/linux/man-pages/man7/user_namespaces.7.html。
1.1. 动手实现
要想实现 User Namespace 的效果,可以把容器内部的 UID 和主机的 UID 进行映射,需要修改的文件是 /proc/<pid>/uid_map
和 /proc/<pid>/gid_map
,这两个文件的格式是
ID-INSIDE-NS ID-OUTSIDE-NS LENGTH
- ID-INSIDE-NS :表示在容器内部显示的 UID 或 GID
- ID-OUTSIDE-NS:表示容器外映射的真实的 UID 和 GID
- LENGTH:表示映射的范围,一般为 1,表示一一对应
比如,下面就是将真实的 uid=1000 的映射为容器内的 uid =0:
$ cat /proc/8353/uid_map
0 1000 1
再比如,下面则表示把 namesapce 内部从 0 开始的 uid 映射到外部从 0 开始的 uid,其最大范围是无符号 32 位整型(下面这条命令是在主机环境中输入的)。
$ cat /proc/$$/uid_map
0 0 4294967295
默认情况,设置了 CLONE_NEWUSER 参数但是没有修改上述两个文件的话,容器中默认情况下显示为 65534,这是因为容器找不到真正的 UID,所以就设置了最大的 UID。如下面的代码所示:
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/mount.h>
#include <sys/capability.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>
#define STACK_SIZE (1024 * 1024)
static char container_stack[STACK_SIZE];
char* const container_args[] = {
"/bin/bash",
NULL
};
int container_main(void* arg) {
printf("Container [%5d] - inside the container!\n", getpid());
printf("Container: eUID = %ld; eGID = %ld, UID=%ld, GID=%ld\n",
(long) geteuid(), (long) getegid(), (long) getuid(), (long) getgid());
printf("Container [%5d] - setup hostname!\n", getpid());
//set hostname
sethostname("container",10);
execv(container_args[0], container_args);
printf("Something's wrong!\n");
return 1;
}
int main() {
const int gid=getgid(), uid=getuid();
printf("Parent: eUID = %ld; eGID = %ld, UID=%ld, GID=%ld\n",
(long) geteuid(), (long) getegid(), (long) getuid(), (long) getgid());
printf("Parent [%5d] - start a container!\n", getpid());
int container_pid = clone(container_main, container_stack+STACK_SIZE,
CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWUSER | SIGCHLD, NULL);
printf("Parent [%5d] - Container [%5d]!\n", getpid(), container_pid);
printf("Parent [%5d] - user/group mapping done!\n", getpid());
waitpid(container_pid, NULL, 0);
printf("Parent - container stopped!\n");
return 0;
}
当我以 dawn 这个用户执行的该程序的时候,那么会显示如下图所示的效果。使用 root 用户的时候是同样的:
接下去,我们要开始来实现映射的效果了,也就是让 dawn 这个用户在容器中显示为 0。代码是几乎完全拿耗子叔的博客上的,链接可见文末:
int pipefd[2];
void set_map(char* file, int inside_id, int outside_id, int len) {
FILE* mapfd = fopen(file, "w");
if (NULL == mapfd) {
perror("open file error");
return;
}
fprintf(mapfd, "%d %d %d", inside_id, outside_id, len);
fclose(mapfd);
}
void set_uid_map(pid_t pid, int inside_id, int outside_id, int len) {
char file[256];
sprintf(file, "/proc/%d/uid_map", pid);
set_map(file, inside_id, outside_id, len);
}
int container_main(void* arg) {
printf("Container [%5d] - inside the container!\n", getpid());
printf("Container: eUID = %ld; eGID = %ld, UID=%ld, GID=%ld\n",
(long) geteuid(), (long) getegid(), (long) getuid(), (long) getgid());
/* 等待父进程通知后再往下执行(进程间的同步) */
char ch;
close(pipefd[1]);
read(pipefd[0], &ch, 1);
printf("Container [%5d] - setup hostname!\n", getpid());
//set hostname
sethostname("container",10);
//remount "/proc" to make sure the "top" and "ps" show container's information
mount("proc", "/proc", "proc", 0, NULL);
execv(container_args[0], container_args);
printf("Something's wrong!\n");
return 1;
}
int main() {
const int gid=getgid(), uid=getuid();
printf("Parent: eUID = %ld; eGID = %ld, UID=%ld, GID=%ld\n",
(long) geteuid(), (long) getegid(), (long) getuid(), (long) getgid());
pipe(pipefd);
printf("Parent [%5d] - start a container!\n", getpid());
int container_pid = clone(container_main, container_stack+STACK_SIZE,
CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWUSER | SIGCHLD, NULL);
printf("Parent [%5d] - Container [%5d]!\n", getpid(), container_pid);
//To map the uid/gid,
// we need edit the /proc/PID/uid_map (or /proc/PID/gid_map) in parent
set_uid_map(container_pid, 0, uid, 1);
printf("Parent [%5d] - user/group mapping done!\n", getpid());
/* 通知子进程 */
close(pipefd[1]);
waitpid(container_pid, NULL, 0);
printf("Parent - container stopped!\n");
return 0;
}
实现的最终效果如图所示,可以看到在容器内部将 dawn 这个用户 UID 显示为了 0(root),但其实这个容器中的 /bin/bash 进程还是以一个普通用户,也就是 dawn 来运行的,只是显示出来的 UID 是 0,所以当查看 /root 目录的时候还是没有权限。
User Namespace 是以普通用户运行的,但是别的 Namespace 需要 root 权限,那么当使用多个 Namespace 该怎么办呢?我们可以先用一般用户创建 User Namespace,然后把这个一般用户映射成 root,那么在容器内用 root 来创建其他的 Namespace。
1.2. 使用 User Namespace 的好处
容器使用 User Namespace 有两个好处:
- 对于用户在容器中自己定义普通用户 uid 的情况,我们只要为每个容器在节点上分配一个 uid 范围,就不会出现在宿主机上 uid 冲突的问题了。
- 容器中 root 用户(uid 0)虽然被映射成宿主机上的普通用户,但是作为容器中的 root,它还是可以有一些 Linux capabilities,这样在容器中还是可以执行一些特权的操作。之外,这个 root 用户对应的是宿主机上 uid 是普通用户,那么即使这个用户逃逸出容器 Namespace,它的执行权限还是有限的。
巨人的肩膀
- 极客时间.《深入剖析 Kubernetes》.张磊."白话容器基础(二):隔离与限制"
- DOCKER基础技术:LINUX NAMESPACE(上)
- DOCKER基础技术:LINUX NAMESPACE(下)。
- 极客时间.《容器实战》