容器须要六项隔离,对应着Linux内核中六种namespace隔离的系统调用。上一篇文章中已经对五种namespace进行了分析:UTS(UNIX Time-sharing System)namespace、IPC(Interprocess Communication)namespace、PID namespace、Mount namespacesdocker
经过上节,咱们了解了PID namespace,当咱们兴致勃勃地在新建的namespace中启动一个“Apache”进程时,却出现了“80端口已被占用”的错误,原来主机上已经运行了一个“Apache”进程。怎么办?这就须要用到network namespace技术进行网络隔离啦。shell
Network namespace主要提供了关于网络资源的隔离,包括网络设备、IPv4和IPv6协议栈、IP路由表、防火墙、/proc/net目录、/sys/class/net目录、端口(socket)等等。一个物理的网络设备最多存在在一个network namespace中,你能够经过建立veth pair(虚拟网络设备对:有两端,相似管道,若是数据从一端传入另外一端也能接收到,反之亦然)在不一样的network namespace间建立通道,以此达到通讯的目的。ubuntu
通常状况下,物理网络设备都分配在最初的root namespace(表示系统默认的namespace,在PID namespace中已经说起)中。可是若是你有多块物理网卡,也能够把其中一块或多块分配给新建立的network namespace。须要注意的是,当新建立的network namespace被释放时(全部内部的进程都终止而且namespace文件没有被挂载或打开),在这个namespace中的物理网卡会返回到root namespace而非建立该进程的父进程所在的network namespace。安全
当咱们说到network namespace时,其实咱们指的未必是真正的网络隔离,而是把网络独立出来,给外部用户一种透明的感受,仿佛跟另一个网络实体在进行通讯。为了达到这个目的,容器的经典作法就是建立一个veth pair,一端放置在新的namespace中,一般命名为eth0,一端放在原先的namespace中链接物理网络设备,再经过网桥把别的设备链接进来或者进行路由转发,以此网络实现通讯的目的。网络
也许有读者会好奇,在创建起veth pair以前,新旧namespace该如何通讯呢?答案是pipe(管道)。咱们以Docker Daemon在启动容器dockerinit的过程为例。Docker Daemon在宿主机上负责建立这个veth pair,经过netlink调用,把一端绑定到docker0网桥上,一端连进新建的network namespace进程中。创建的过程当中,Docker Daemon和dockerinit就经过pipe进行通讯,当Docker Daemon完成veth-pair的建立以前,dockerinit在管道的另外一端循环等待,直到管道另外一端传来Docker Daemon关于veth设备的信息,并关闭管道。dockerinit才结束等待的过程,并把它的“eth0”启动起来。整个效果相似下图所示。app
图2 Docker网络示意图socket
跟其余namespace相似,对network namespace的使用其实就是在建立的时候添加CLONE_NEWNET标识位。也能够经过命令行工具ip建立network namespace。在代码中创建和测试network namespace较为复杂,因此下文主要经过ip命令直观的感觉整个network namespace网络创建和配置的过程。ide
首先咱们能够建立一个命名为test_ns的network namespace。函数
# ip netns add test_ns
当ip命令工具建立一个network namespace时,会默认建立一个回环设备(loopback interface:lo),并在/var/run/netns目录下绑定一个挂载点,这就保证了就算network namespace中没有进程在运行也不会被释放,也给系统管理员对新建立的network namespace进行配置提供了充足的时间。工具
经过ip netns exec命令能够在新建立的network namespace下运行网络管理命令。
# ip netns exec test_ns ip link list 3: lo:mtu 16436 qdisc noop state DOWN link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
上面的命令为咱们展现了新建的namespace下可见的网络连接,能够看到状态是DOWN,须要再经过命令去启动。能够看到,此时执行ping命令是无效的。
# ip netns exec test_ns ping 127.0.0.1 connect: Network is unreachable
启动命令以下,能够看到启动后再测试就能够ping通。
# ip netns exec test_ns ip link set dev lo up # ip netns exec test_ns ping 127.0.0.1 PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data. 64 bytes from 127.0.0.1: icmp_req=1 ttl=64 time=0.050 ms ...
这样只是启动了本地的回环,要实现与外部namespace进行通讯还须要再建一个网络设备对,命令以下。
# ip link add veth0 type veth peer name veth1 # ip link set veth1 netns test_ns # ip netns exec test_ns ifconfig veth1 10.1.1.1/24 up # ifconfig veth0 10.1.1.2/24 up
第一条命令建立了一个网络设备对,全部发送到veth0的包veth1也能接收到,反之亦然。
第二条命令则是把veth1这一端分配到test_ns这个network namespace。
第3、第四条命令分别给test_ns内部和外部的网络设备配置IP,veth1的IP为10.1.1.1,veth0的IP为10.1.1.2。
此时两边就能够互相连通了,效果以下。
# ping 10.1.1.1 PING 10.1.1.1 (10.1.1.1) 56(84) bytes of data. 64 bytes from 10.1.1.1: icmp_req=1 ttl=64 time=0.095 ms ... # ip netns exec test_ns ping 10.1.1.2 PING 10.1.1.2 (10.1.1.2) 56(84) bytes of data. 64 bytes from 10.1.1.2: icmp_req=1 ttl=64 time=0.049 ms ...
读者有兴趣能够经过下面的命令查看,新的test_ns有着本身独立的路由和iptables。
ip netns exec test_ns route ip netns exec test_ns iptables -L
路由表中只有一条通向10.1.1.2的规则,此时若是要链接外网确定是不可能的,你能够经过创建网桥或者NAT映射来决定这个问题。若是你对此很是感兴趣,能够阅读Docker网络相关文章进行更深刻的讲解。
作完这些实验,你还能够经过下面的命令删除这个network namespace。
# ip netns delete netns1
这条命令会移除以前的挂载,可是若是namespace自己还有进程运行,namespace还会存在下去,直到进程运行结束。
经过network namespace咱们能够了解到,实际上内核建立了network namespace之后,真的是获得了一个被隔离的网络。可是咱们实际上须要的不是这种彻底的隔离,而是一个对用户来讲透明独立的网络实体,咱们须要与这个实体通讯。因此Docker的网络在起步阶段给人一种很是难用的感受,由于一切都要本身去实现、去配置。你须要一个网桥或者NAT链接广域网,你须要配置路由规则与宿主机中其余容器进行必要的隔离,你甚至还须要配置防火墙以保证安全等等。所幸这一切已经有了较为成熟的方案,咱们会在Docker网络部分进行详细的讲解。
7. User namespacesUser namespace主要隔离了安全相关的标识符(identifiers)和属性(attributes),包括用户ID、用户组ID、root目录、key(指密钥)以及特殊权限。说得通俗一点,一个普通用户的进程经过clone()建立的新进程在新user namespace中能够拥有不一样的用户和用户组。这意味着一个进程在容器外属于一个没有特权的普通用户,可是他建立的容器进程却属于拥有全部权限的超级用户,这个技术为容器提供了极大的自由。
User namespace是目前的六个namespace中最后一个支持的,而且直到Linux内核3.8版本的时候还未彻底实现(还有部分文件系统不支持)。由于user namespace实际上并不算彻底成熟,不少发行版担忧安全问题,在编译内核的时候并未开启USER_NS。实际上目前Docker也还不支持user namespace,可是预留了相应接口,相信在不久后就会支持这一特性。因此在进行接下来的代码实验时,请确保你系统的Linux内核版本高于3.8而且内核编译时开启了USER_NS(若是你不会选择,可使用Ubuntu14.04)。
Linux中,特权用户的user ID就是0,演示的最终咱们将看到user ID非0的进程启动user namespace后user ID能够变为0。使用user namespace的方法跟别的namespace相同,即调用clone()或unshare()时加入CLONE_NEWUSER标识位。老样子,修改代码并另存为userns.c,为了看到用户权限(Capabilities),可能你还须要安装一下libcap-dev包。
首先包含如下头文件以调用Capabilities包。
#include
其次在子进程函数中加入geteuid()和getegid()获得namespace内部的user ID,其次经过cap_get_proc()获得当前进程的用户拥有的权限,并经过cap_to_text()输出。
int child_main(void* args) { printf("在子进程中!\n"); cap_t caps; printf("eUID = %ld; eGID = %ld; ", (long) geteuid(), (long) getegid()); caps = cap_get_proc(); printf("capabilities: %s\n", cap_to_text(caps, NULL)); execv(child_args[0], child_args); return 1; }
在主函数的clone()调用中加入咱们熟悉的标识符。
//[...] int child_pid = clone(child_main, child_stack+STACK_SIZE, CLONE_NEWUSER | SIGCHLD, NULL); //[...]
至此,第一部分的代码修改就结束了。在编译以前咱们先查看一下当前用户的uid和guid,请注意此时咱们是普通用户。
$ id -u 1000 $ id -g 1000
而后咱们开始编译运行,并进行新建的user namespace,你会发现shell提示符前的用户名已经变为nobody。
sun@ubuntu$ gcc userns.c -Wall -lcap -o userns.o && ./userns.o 程序开始: 在子进程中! eUID = 65534; eGID = 65534; capabilities: = cap_chown,cap_dac_override,[...]37+ep <<--此处省略部分输出,已拥有所有权限 nobody@ubuntu$="" <="" pre="">
经过验证咱们能够获得如下信息。
user namespace被建立后,第一个进程被赋予了该namespace中的所有权限,这样这个init进程就能够完成全部必要的初始化工做,而不会因权限不足而出现错误。
咱们看到namespace内部看到的UID和GID已经与外部不一样了,默认显示为65534,表示还没有与外部namespace用户映射。咱们须要对user namespace内部的这个初始user和其外部namespace某个用户创建映射,这样能够保证当涉及到一些对外部namespace的操做时,系统能够检验其权限(好比发送一个信号或操做某个文件)。一样用户组也要创建映射。
还有一点虽然不能从输出中看出来,可是值得注意。用户在新namespace中有所有权限,可是他在建立他的父namespace中不含任何权限。就算调用和建立他的进程有所有权限也是如此。因此哪怕是root用户调用了clone()在user namespace中建立出的新用户在外部也没有任何权限。
最后,user namespace的建立实际上是一个层层嵌套的树状结构。最上层的根节点就是root namespace,新建立的每一个user namespace都有一个父节点user namespace以及零个或多个子节点user namespace,这一点与PID namespace很是类似。
接下来咱们就要进行用户绑定操做,经过在/proc/[pid]/uid_map和/proc/[pid]/gid_map两个文件中写入对应的绑定信息能够实现这一点,格式以下。
ID-inside-ns ID-outside-ns length
写这两个文件须要注意如下几点。
这两个文件只容许由拥有该user namespace中CAP_SETUID权限的进程写入一次,不容许修改。
写入的进程必须是该user namespace的父namespace或者子namespace。
第一个字段ID-inside-ns表示新建的user namespace中对应的user/group ID,第二个字段ID-outside-ns表示namespace外部映射的user/group ID。最后一个字段表示映射范围,一般填1,表示只映射一个,若是填大于1的值,则按顺序创建一一映射。
明白了上述原理,咱们再次修改代码,添加设置uid和guid的函数。
//[...] 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* args) { cap_t caps; printf("在子进程中!\n"); set_uid_map(getpid(), 0, 1000, 1); set_gid_map(getpid(), 0, 1000, 1); printf("eUID = %ld; eGID = %ld; ", (long) geteuid(), (long) getegid()); caps = cap_get_proc(); printf("capabilities: %s\n", cap_to_text(caps, NULL)); execv(child_args[0], child_args); return 1; } //[...]
编译后便可看到user已经变成了root。
$ gcc userns.c -Wall -lcap -o usernc.o && ./usernc.o 程序开始: 在子进程中! eUID = 0; eGID = 0; capabilities: = [...],37+ep root@ubuntu:~#
至此,你就已经完成了绑定的工做,能够看到演示全程都是在普通用户下执行的。最终实现了在user namespace中成为了root而对应到外面的是一个uid为1000的普通用户。
若是你要把user namespace与其余namespace混合使用,那么依旧须要root权限。解决方案能够是先以普通用户身份建立user namespace,而后在新建的namespace中做为root再clone()进程加入其余类型的namespace隔离。
讲完了user namespace,咱们再来谈谈Docker。虽然Docker目前还没有使用user namespace,可是他用到了咱们在user namespace中说起的Capabilities机制。从内核2.2版本开始,Linux把原来和超级用户相关的高级权限划分红为不一样的单元,称为Capability。这样管理员就能够独立对特定的Capability进行使能或禁止。Docker虽然没有使用user namespace,可是他能够禁用容器中不须要的Capability,一次在必定程度上增强容器安全性。
固然,说到安全,namespace的六项隔离看似全面,实际上依旧没有彻底隔离Linux的资源,好比SELinux、 Cgroups以及/sys、/proc/sys、/dev/sd*等目录下的资源。关于安全的更多讨论和讲解,咱们会在后文中接着探讨。
8. 总结本文从namespace使用的API开始,结合Docker逐步对六个namespace进行讲解。相信把讲解过程当中全部的代码整合起来,你也能实现一个属于本身的“shell”容器了。虽然namespace技术使用起来很是简单,可是要真正把容器作到安全易用却并不是易事。PID namespace中,咱们要实现一个完善的init进程来维护好全部进程;network namespace中,咱们还有复杂的路由表和iptables规则没有配置;user namespace中还有不少权限上的问题须要考虑等等。其中有些方面Docker已经作的很好,有些方面也才刚刚开始。但愿经过本文,能为你们更好的理解Docker背后运行的原理提供帮助。
9. 做者简介孙健波,浙江大学SEL实验室硕士研究生,目前在云平台团队从事科研和开发工做。浙大团队对PaaS、Docker、大数据和主流开源云计算技术有深刻的研究和二次开发经验,团队现将部分技术文章贡献出来,但愿能对读者有所帮助。