最近在做 Kubernetes Security 相关的工作。作为一个 Kubernetes 新手,刚看到 yaml 文件中的 namespace 时,很容易和 Linux 的 Namespace 搞混。加上在读 Kubernetes 文档的时候,总时不时看到 Linux Namespace 的字样就更加困惑起来。以此为契机,正好把 Linux Namespace 再好好梳理一下,解开心中的谜团:
- Linux 的 Namespace 是如何实现资源的隔离
- Linux 下哪些资源可以按照 Namespace 组织,如何组织
- Container 哪些地方用到了 Linux Namespace
英雄诞生 链接到标题
传统的 Linux 及其衍生的 UNIX 变体中,许多资源是全局管理的。例如,系统中的所有进程按照惯例是通过 PID 标识的,这意味着内核必须管理一个全局的 PID 列表。而且,所有调用者通过 uname 系统调用返回的系统相关信息(包括系统名称和有关内核的一些信息)都是相同的。用户 ID 的管理方式类似,即各个用户是通过一个全局唯一的 UID 号标识。
全局 ID 给内核极大的选择权,可以通过 ID 来允许或拒绝某些权限。UID 为 0 的 root 用户可以做人和事,但其他 UID 会受限。例如 UID 为 n 的用户,不允许杀死属于用户 m 的进程(m ≠ n )。但这不能防止用户看到彼此,即用户 n 可以看到另一个用户 m 也在计算机上活动。这种效果在某些情况下是不可取的。
Namespace 提供了虚拟化的一种轻量级形式,它只使用一个内核在一台物理机上运行,就可以将上述的全局资源组织起来。这使得可以将一组进程放置到容器中,各个容器彼此隔离。隔离可以使容器的成员与其他容器毫无关系。但也可以通过允许容器进行一定的共享,来降低容器之间的分隔。例如,容器可以设置为使用自身的 PID 集合,但仍然与其他容器共享部分文件系统。
实现 链接到标题
Namespace 的实现需要两个部分:
- 每个子系统的 namespace 结构,将此前所有的全局组件包装到 namespace 中
- 将给定进程关联到所属各个 namespace 的机制
本文采用的源码是内核 5.4
Linux内核中进程用 task_struct 结构体表示,我们称为进程描述符。该结构体定义在 sched.h 中,记录了进程相关的所有信息,比如进程地址空间,进程状态,打开的文件等。
// sched.h
struct task_struct {
/* Filesystem information: */
struct fs_struct *fs;
/* Open file information: */
struct files_struct *files;
/* Namespaces: */
struct nsproxy *nsproxy;
// ...
}
这里我们重点关注 nsproxy 变量。nsproxy 结构体包含指向进程所属 Namespace 的所有指针,分别是:
| Namespace | 系统调用参数 | 隔离内容 |
|---|---|---|
| UTS | CLONE_NEWUTS | 主机名与域名 |
| IPC | CLONE_NEWIPC | 信号量、消息队列和共享内存 |
| PID | CLONE_NEWPID | 进程编号 |
| Network | CLONE_NEWNET | 网络设备、网络栈、端口等等 |
| Mount | CLONE_NEWNS | 挂载点(文件系统) |
| User | CLONE_NEWUSER | 用户和用户组 |
nsproxy 由 namespace 下的所有进程共享,namespace 再封装了各种资源,进程只能查看和操作绑定在 namespace 的资源。
// nsproxy.h
struct nsproxy {
atomic_t count;
struct uts_namespace *uts_ns;
struct ipc_namespace *ipc_ns;
struct mnt_namespace *mnt_ns;
struct pid_namespace *pid_ns_for_children;
struct net *net_ns;
struct cgroup_namespace *cgroup_ns;
};
这里特别说明下:
- 进程的 pid namespace 通过
task_active_pid_ns获取。nsproxy结构中的pid_namespae是给子进程使用的。 - User namespace 没有直接出现在
nsproxy中,但**_namespace都有包含 user namespace 的指针。 - 实际上,内核从 4.6 开始引入了 cgroup namespace,本文就先掠过不表,留待以后探索。
Namespace 的 API 链接到标题
有两种方法创建 namespace:
- 在用
fork或clone系统调用创建新进程时,有特定的选项可以控制是与父进程共享 namespace,还是建立新的 namespace。 unshare系统调用将进程的某些部分从父进程分离,其中也包括 namespace。
这底下用到三个函数 clone()、setns() 以及 unshare(),让我们逐个解析。
clone() 创建新 namespace
链接到标题
clone() 函数的 manual 在 CLONE(2)
int clone(int (*fn)(void *), void *child_stack,
int flags, void *arg, ...
/* pid_t *ptid, void *newtls, pid_t *ctid */ );
clone() 可以看作是传统 UNIX fork 的通用版本,两者都创建一个新的进程,也创建了一个新的 namespace。不同之处在于,clone() 允许子进程共享父进程的部分执行上下文,比如虚拟地址空间、文件描述符等。通过 flags 参数,我们可以控制父子进程之间共享的内容。
clone() 的几个参数:
fn传入子进程运行的主函数child_stack传入子进程使用的栈空间,因为子进程可能和父进程共享内存flags表示使用哪些CLONE_*标志位arg即传入fn函数的参数
setns() 加入已有 namespace
链接到标题
通过 setns() 可以加入一个已经存在的 namespace。
int setns(int fd, int nstype);
其中,
fd是指向/proc/[pid]/ns/目录的文件描述符nstype使用 CLONE_* 标志位约束了 namespace 的类型
unshare() 离开 namespace
链接到标题
int unshare(int flags);
unshare() 将进程(线程)从它被其他进程(线程)共享的执行上下文中分离出来。flags 参数是 CLONE_* 的位掩码。
/proc/[pid]/ns/ 目录 链接到标题
前文说到了 /proc/[pid]/ns/ 目录,每个进程在该目录下都有一个子目录,标识着所属的 namespace。
$ ls -l /proc/$$/ns
total 0
lrwxrwxrwx. 1 mtk mtk 0 Apr 28 12:46 cgroup -> cgroup:[4026531835]
lrwxrwxrwx. 1 mtk mtk 0 Apr 28 12:46 ipc -> ipc:[4026531839]
lrwxrwxrwx. 1 mtk mtk 0 Apr 28 12:46 mnt -> mnt:[4026531840]
lrwxrwxrwx. 1 mtk mtk 0 Apr 28 12:46 net -> net:[4026531969]
lrwxrwxrwx. 1 mtk mtk 0 Apr 28 12:46 pid -> pid:[4026531836]
lrwxrwxrwx. 1 mtk mtk 0 Apr 28 12:46 pid_for_children -> pid:[4026531834]
lrwxrwxrwx. 1 mtk mtk 0 Apr 28 12:46 user -> user:[4026531837]
rwxrwxrwx. 1 mtk mtk 0 Apr 28 12:46 uts -> uts:[4026531838]
下面,我们对 6 种 namespace 一一拆解。
PID Namespace 链接到标题
每个进程都有一个 PID,PID namespace 将 PID 隔离开来。这意味着在不同 PID namespace 下的进程可以有相同的 PID。Container 可以依靠 PID namespace 来 suspend 或 resume 一个 namespace 下的进程集合,而不影响其他 namespace 的进程。
fork 和 clone 创建的进程,带有两个 PID,其中一个是 namespace 中唯一 PID,另一个是 host 系统上的 PID。PID namespace 的 PID 从 2 开始分配,1 这个位置留给 init 进程,它是所有孤儿进程的父进程。如果 init 进程终止了,那么内核会通过 SIGKILL 来终止该 namespace 下的所有进程。
PID namespace 可以组织为层次,也可以为非层次。内核为所有的 PID namespace 维护了一个树状结构,最顶层的是系统初始时创建的 root namespace。在层次组织的 namespace 中,父 namespace 能够看到子 namespace 的所有进程,因为后者进程的 PID 会映射到父 namespace 中。
我们来看下 pid_namespace 结构体:
struct pid_namespace {
unsigned int pid_allocated;
struct task_struct *child_reaper;
struct pid_namespace *parent;
struct user_namespace *user_ns;
// ...
} __randomize_layout;
它包括:
- PID
- 子进程的指针
- 父 PID namespace 的指针
- 所属 User namespace 的指针
Mount Namespace 链接到标题
Mount namespace 隔离的是挂载点,也就是说不同 namesapce 下的进程看到不同的文件系统视图。因此,Mount namespace 用来实现容器中文件系统的隔离。引入 Mount namespace 之后,mount 和 umount 这两个系统调用就不会在 global 的 挂载点上执行了。
Mount namespace 是内核 2.4.19 时引进的,代表常量是 CLONE_NEWNS,是没有考虑到之后的扩展性。
struct mnt_namespace {
atomic_t count;
struct ns_common ns;
struct mount * root;
struct list_head list;
struct user_namespace *user_ns;
struct ucounts *ucounts;
u64 seq; /* Sequence number to prevent loops */
wait_queue_head_t poll;
u64 event;
unsigned int mounts; /* # of mounts in the namespace */
unsigned int pending_mounts;
} __randomize_layout;
mnt_namespace 结构体中有:
- User namespace 的指针
- 所有 mount 的文件树的 root 指针
User Namespace 链接到标题
User namespace 隔离了 UID 和 GID。换句话说,进程的 UID 和 GID 在 namespace 内外可以不同。这意味着,一个进程可以在 user namespace 之外有一个 unprivileged UID,同时在一个 user namespace 内有为 0 的 privileged UID。
struct user_namespace {
struct uid_gid_map uid_map;
struct uid_gid_map gid_map;
struct uid_gid_map projid_map;
atomic_t count;
struct user_namespace *parent;
int level;
kuid_t owner;
kgid_t group;
struct ns_common ns;
unsigned long flags;
struct ucounts *ucounts;
int ucount_max[UCOUNT_COUNTS];
} __randomize_layout;
user_namespace 中有:
- 父 user namespace 的指针
- UID 和 GID 的 map
Network Namespace 链接到标题
Network namespace 隔离了网络资源,包括网络设备、IP 地址、IP 路由表、/proc/net 目录、端口号等等。
对容器来说,每个容器就可以有自己的网络设备,containerized app 也可以绑定到自己的端口空间。Host 机器上的路由规则也可以把数据包传送给指定的容器(关联到某个网络设备)。
struct net {
struct list_head list; /* list of network namespaces */
struct user_namespace *user_ns; /* Owning user namespace */
struct netns_core core;
struct netns_mib mib;
struct netns_packet packet;
struct netns_unix unx;
struct netns_nexthop nexthop;
struct netns_ipv4 ipv4;
}
net 结构中包括:
- 各种 net_xxx 的网络设备
- 所属的 user namespace
- Network namespace 的列表
UTS Namespace 链接到标题
UTS 隔离 nodename 和 domainname 系统标识符。通过 uname 调用可以看到这两个标识符的值。借助 UTS namespace,容器可以有自己的 hostname 和 NIS domain name。UTS 是 “UNIX Time-sharing System” 的缩写。
struct uts_namespace {
struct kref kref;
struct new_utsname name;
struct user_namespace *user_ns;
struct ucounts *ucounts;
struct ns_common ns;
} __randomize_layout;
uts_namespace 结构体也是最简单的,只有所属的 user namespace。
IPC Namespace 链接到标题
IPC namespace 隔离了 System V IPC 和 POSIX 消息队列。这两个都是进程通信的资源。只有在一个 IPC namespace 下的进程才能够通信。所以在同一个 IPC namespace 下的进程彼此可见,而与其他的 IPC namespace 下的进程则互相不可见。
struct ipc_namespace {
unsigned int msg_ctlmax;
unsigned int msg_ctlmnb;
unsigned int msg_ctlmni;
atomic_t msg_bytes;
atomic_t msg_hdrs;
size_t shm_ctlmax;
size_t shm_ctlall;
unsigned int mq_queues_count;
/* next fields are set through sysctl */
unsigned int mq_queues_max; /* initialized to DFLT_QUEUESMAX */
unsigned int mq_msg_max; /* initialized to DFLT_MSGMAX */
unsigned int mq_msgsize_max; /* initialized to DFLT_MSGSIZEMAX */
unsigned int mq_msg_default;
unsigned int mq_msgsize_default;
/* user_ns which owns the ipc ns */
struct user_namespace *user_ns;
}
Reference