Docker 基础技术:Linux Namespace(下)

在 Docker基础技术:Linux Namespace(上篇)中我们了解了,UTD、IPC、PID、Mount 四个namespace,我们模仿Docker做了一个相当相当山寨的镜像。在这一篇中,主要想向大家介绍Linux的User和Network的Namespace。

好,下面我们就介绍一下还剩下的这两个Namespace。

User Namespace

User Namespace主要是用了CLONE_NEWUSER的参数。使用了这个参数后,内部看到的UID和GID已经与外部不同了,默认显示为65534。那是因为容器找不到其真正的UID所以,设置上了最大的UID(其设置定义在/proc/sys/kernel/overflowuid)。

要把容器中的uid和真实系统的uid给映射在一起,需要修改 /proc/ /uid_map 和/proc//gid_map 这两个文件。这两个文件的格式为:

ID-inside-ns ID-outside-ns length

其中:

  • 第一个字段ID-inside-ns表示在容器显示的UID或GID,
  • 第二个字段ID-outside-ns表示容器外映射的真实的UID或GID。
  • 第三个字段表示映射的范围,一般填1,表示一一对应。

比如,把真实的uid=1000映射成容器内的uid=0


  1. $cat/proc/2465/uid_map 
  2.          0       1000          1 

再比如下面的示例:表示把namespace内部从0开始的uid映射到外部从0开始的uid,其最大范围是无符号32位整形


  1. $cat/proc/$$/uid_map 
  2.          0          0          4294967295 

另外,需要注意的是:

  • 写这两个文件的进程需要这个namespace中的CAP_SETUID (CAP_SETGID)权限(可参看Capabilities)
  • 写入的进程必须是此user namespace的父或子的user namespace进程。
  • 另外需要满如下条件之一:1)父进程将effective uid/gid映射到子进程的user namespace中,2)父进程如果有CAP_SETUID/CAP_SETGID权限,那么它将可以映射到父进程中的任一uid/gid。

这些规则看着都烦,我们来看程序吧(下面的程序有点长,但是非常简单,如果你读过《Unix网络编程》上卷,你应该可以看懂):


  1. #define _GNU_SOURCE 
  2. #include <stdio.h> 
  3. #include <stdlib.h> 
  4. #include <sys/types.h> 
  5. #include <sys/wait.h> 
  6. #include <sys/mount.h> 
  7. #include <sys/capability.h> 
  8. #include <stdio.h> 
  9. #include <sched.h> 
  10. #include <signal.h> 
  11. #include <unistd.h> 
  12. #define STACK_SIZE (1024 * 1024) 
  13. staticcharcontainer_stack[STACK_SIZE]; 
  14. char*constcontainer_args[] = { 
  15.     “/bin/bash”, 
  16.     NULL 
  17. }; 
  18. intpipefd[2]; 
  19. voidset_map(char* file,intinside_id,intoutside_id,intlen) { 
  20.     FILE* mapfd =fopen(file,”w”); 
  21.     if(NULL == mapfd) { 
  22.         perror(“open file error”); 
  23.         return; 
  24.     } 
  25.     fprintf(mapfd,”%d %d %d”, inside_id, outside_id, len); 
  26.     fclose(mapfd); 
  27. voidset_uid_map(pid_t pid,intinside_id,intoutside_id,intlen) { 
  28.     charfile[256]; 
  29.     sprintf(file,”/proc/%d/uid_map”, pid); 
  30.     set_map(file, inside_id, outside_id, len); 
  31. voidset_gid_map(pid_t pid,intinside_id,intoutside_id,intlen) { 
  32.     charfile[256]; 
  33.     sprintf(file,”/proc/%d/gid_map”, pid); 
  34.     set_map(file, inside_id, outside_id, len); 
  35. intcontainer_main(void* arg) 
  36.     printf(“Container [%5d] – inside the container!\n”, getpid()); 
  37.     printf(“Container: eUID = %ld;  eGID = %ld, UID=%ld, GID=%ld\n”, 
  38.             (long) geteuid(), (long) getegid(), (long) getuid(), (long) getgid()); 
  39.     /* 等待父进程通知后再往下执行(进程间的同步) */ 
  40.     charch; 
  41.     close(pipefd[1]); 
  42.     read(pipefd[0], &ch, 1); 
  43.     printf(“Container [%5d] – setup hostname!\n”, getpid()); 
  44.     //set hostname 
  45.     sethostname(“container”,10); 
  46.     //remount “/proc” to make sure the “top” and “ps” show container’s information 
  47.     mount(“proc”,”/proc”,”proc”, 0, NULL); 
  48.     execv(container_args[0], container_args); 
  49.     printf(“Something’s wrong!\n”); 
  50.     return1; 
  51. intmain() 
  52.     constintgid=getgid(), uid=getuid(); 
  53.     printf(“Parent: eUID = %ld;  eGID = %ld, UID=%ld, GID=%ld\n”, 
  54.             (long) geteuid(), (long) getegid(), (long) getuid(), (long) getgid()); 
  55.     pipe(pipefd); 
  56.     printf(“Parent [%5d] – start a container!\n”, getpid()); 
  57.     intcontainer_pid = clone(container_main, container_stack+STACK_SIZE, 
  58.             CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWUSER | SIGCHLD, NULL); 
  59.     printf(“Parent [%5d] – Container [%5d]!\n”, getpid(), container_pid); 
  60.     //To map the uid/gid, 
  61.     //   we need edit the /proc/PID/uid_map (or /proc/PID/gid_map) in parent 
  62.     //The file format is 
  63.     //   ID-inside-ns   ID-outside-ns   length 
  64.     //if no mapping, 
  65.     //   the uid will be taken from /proc/sys/kernel/overflowuid 
  66.     //   the gid will be taken from /proc/sys/kernel/overflowgid 
  67.     set_uid_map(container_pid, 0, uid, 1); 
  68.     set_gid_map(container_pid, 0, gid, 1); 
  69.     printf(“Parent [%5d] – user/group mapping done!\n”, getpid()); 
  70.     /* 通知子进程 */ 
  71.     close(pipefd[1]); 
  72.     waitpid(container_pid, NULL, 0); 
  73.     printf(“Parent – container stopped!\n”); 
  74.     return0; 

上面的程序,我们用了一个pipe来对父子进程进行同步,为什么要这样做?因为子进程中有一个execv的系统调用,这个系统调用会把当前子进程的进程空间给全部覆盖掉,我们希望在execv之前就做好user namespace的uid/gid的映射,这样,execv运行的/bin/bash就会因为我们设置了uid为0的inside-uid而变成#号的提示符。

整个程序的运行效果如下:


  1. hchen@ubuntu:~$id 
  2. uid=1000(hchen) gid=1000(hchen)groups=1000(hchen) 
  3. hchen@ubuntu:~$ ./user#<–以hchen用户运行 
  4. Parent: eUID = 1000;  eGID = 1000, UID=1000, GID=1000 
  5. Parent [ 3262] – start a container! 
  6. Parent [ 3262] – Container [ 3263]! 
  7. Parent [ 3262] – user/groupmappingdone! 
  8. Container [    1] – inside the container! 
  9. Container: eUID = 0;  eGID = 0, UID=0, GID=0#<—Container里的UID/GID都为0了 
  10. Container [    1] – setuphostname! 
  11. root@container:~# id #<—-我们可以看到容器里的用户和命令行提示符是root用户了 
  12. uid=0(root) gid=0(root)groups=0(root),65534(nogroup) 

虽然容器里是root,但其实这个容器的/bin/bash进程是以一个普通用户hchen来运行的。这样一来,我们容器的安全性会得到提高。

我们注意到,User Namespace是以普通用户运行,但是别的Namespace需要root权限,那么,如果我要同时使用多个Namespace,该怎么办呢?一般来说,我们先用一般用户创建User Namespace,然后把这个一般用户映射成root,在容器内用root来创建其它的Namesapce。

Network Namespace

Network的Namespace比较啰嗦。在Linux下,我们一般用ip命令创建Network Namespace(Docker的源码中,它没有用ip命令,而是自己实现了ip命令内的一些功能——是用了Raw Socket发些“奇怪”的数据,呵呵)。这里,我还是用ip命令讲解一下。

首先,我们先看个图,下面这个图基本上就是Docker在宿主机上的网络示意图(其中的物理网卡并不准确,因为docker可能会运行在一个VM中,所以,这里所谓的“物理网卡”其实也就是一个有可以路由的IP的网卡)



上图中,Docker使用了一个私有网段,172.40.1.0,docker还可能会使用10.0.0.0和192.168.0.0这两个私有网段,关键看你的路由表中是否配置了,如果没有配置,就会使用,如果你的路由表配置了所有私有网段,那么docker启动时就会出错了。

当你启动一个Docker容器后,你可以使用ip link show或ip addr show来查看当前宿主机的网络情况(我们可以看到有一个docker0,还有一个veth22a38e6的虚拟网卡——给容器用的):


  1. hchen@ubuntu:~$ ip link show 
  2. 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state … 
  3.     link/loopback00:00:00:00:00:00 brd 00:00:00:00:00:00 
  4. 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc … 
  5.     link/ether00:0c:29:b7:67:7d brd ff:ff:ff:ff:ff:ff 
  6. 3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 … 
  7.     link/ether56:84:7a:fe:97:99 brd ff:ff:ff:ff:ff:ff 
  8. 5: veth22a38e6: <BROADCAST,UP,LOWER_UP> mtu 1500 qdisc … 
  9.     link/ether8e:30:2a:ac:8c:d1 brd ff:ff:ff:ff:ff:ff 

那么,要做成这个样子应该怎么办呢?我们来看一组命令:


  1. ## 首先,我们先增加一个网桥lxcbr0,模仿docker0 
  2. brctl addbr lxcbr0 
  3. brctl stp lxcbr0 off 
  4. ifconfiglxcbr0 192.168.10.1/24up#为网桥设置IP地址 
  5. ## 接下来,我们要创建一个network namespace – ns1 
  6. # 增加一个namesapce 命令为 ns1 (使用ip netns add命令) 
  7. ip netns add ns1 
  8. # 激活namespace中的loopback,即127.0.0.1(使用ip netns exec ns1来操作ns1中的命令) 
  9. ip netnsexecns1   ip linksetdev lo up 
  10. ## 然后,我们需要增加一对虚拟网卡 
  11. # 增加一个pair虚拟网卡,注意其中的veth类型,其中一个网卡要按进容器中 
  12. ip link add veth-ns1typeveth peer name lxcbr0.1 
  13. # 把 veth-ns1 按到namespace ns1中,这样容器中就会有一个新的网卡了 
  14. ip linksetveth-ns1 netns ns1 
  15. # 把容器里的 veth-ns1改名为 eth0 (容器外会冲突,容器内就不会了) 
  16. ip netnsexecns1  ip linksetdev veth-ns1 name eth0 
  17. # 为容器中的网卡分配一个IP地址,并激活它 
  18. ip netnsexecns1ifconfigeth0 192.168.10.11/24up 
  19. # 上面我们把veth-ns1这个网卡按到了容器中,然后我们要把lxcbr0.1添加上网桥上 
  20. brctl addif lxcbr0 lxcbr0.1 
  21. # 为容器增加一个路由规则,让容器可以访问外面的网络 
  22. ip netnsexecns1     ip route add default via 192.168.10.1 
  23. # 在/etc/netns下创建network namespce名称为ns1的目录, 
  24. # 然后为这个namespace设置resolv.conf,这样,容器内就可以访问域名了 
  25. mkdir-p/etc/netns/ns1 
  26. echo”nameserver 8.8.8.8″>/etc/netns/ns1/resolv.conf 

上面基本上就是docker网络的原理了,只不过,

  • Docker的resolv.conf没有用这样的方式,而是用了上篇中的Mount Namesapce的那种方式
  • 另外,docker是用进程的PID来做Network Namespace的名称的。

了解了这些后,你甚至可以为正在运行的docker容器增加一个新的网卡:


  1.      
  2. ip link add peerAtypeveth peer name peerB 
  3. brctl addif docker0 peerA 
  4. ip linksetpeerA up 
  5. ip linksetpeerB netns ${container-pid} 
  6. ip netnsexec${container-pid} ip linksetdev peerB name eth1 
  7. ip netnsexec${container-pid} ip linkseteth1 up ; 
  8. ip netnsexec${container-pid} ip addr add ${ROUTEABLE_IP} dev eth1 ; 

上面的示例是我们为正在运行的docker容器,增加一个eth1的网卡,并给了一个静态的可被外部访问到的IP地址。

这个需要把外部的“物理网卡”配置成混杂模式,这样这个eth1网卡就会向外通过ARP协议发送自己的Mac地址,然后外部的交换机就会把到这个IP地址的包转到“物理网卡”上,因为是混杂模式,所以eth1就能收到相关的数据,一看,是自己的,那么就收到。这样,Docker容器的网络就和外部通了。

当然,无论是Docker的NAT方式,还是混杂模式都会有性能上的问题,NAT不用说了,存在一个转发的开销,混杂模式呢,网卡上收到的负载都会完全交给所有的虚拟网卡上,于是就算一个网卡上没有数据,但也会被其它网卡上的数据所影响。

这两种方式都不够完美,我们知道,真正解决这种网络问题需要使用VLAN技术,于是Google的同学们为Linux内核实现了一个IPVLAN的驱动,这基本上就是为Docker量身定制的。

Namespace文件

上面就是目前Linux Namespace的玩法。 现在,我来看一下其它的相关东西。

让我们运行一下上篇中的那个pid.mnt的程序(也就是PID Namespace中那个mount proc的程序),然后不要退出。


  1. $ sudo ./pid.mnt 
  2. [sudo] passwordforhchen: 
  3. Parent [ 4599] – start a container! 
  4. Container [    1] – inside the container! 

我们到另一个shell中查看一下父子进程的PID:


  1. hchen@ubuntu:~$ pstree -p 4599 
  2. pid.mnt(4599)───bash(4600) 

我们可以到proc下(/proc//ns)查看进程的各个namespace的id(内核版本需要3.8以上)。

下面是父进程的:


  1. hchen@ubuntu:~$sudols-l/proc/4599/ns 
  2. total 0 
  3. lrwxrwxrwx 1 root root 0  4月  7 22:01 ipc -> ipc:[4026531839] 
  4. lrwxrwxrwx 1 root root 0  4月  7 22:01 mnt -> mnt:[4026531840] 
  5. lrwxrwxrwx 1 root root 0  4月  7 22:01 net -> net:[4026531956] 
  6. lrwxrwxrwx 1 root root 0  4月  7 22:01 pid -> pid:[4026531836] 
  7. lrwxrwxrwx 1 root root 0  4月  7 22:01 user -> user:[4026531837] 
  8. lrwxrwxrwx 1 root root 0  4月  7 22:01 uts -> uts:[4026531838] 

下面是子进程的:


  1. hchen@ubuntu:~$sudols-l/proc/4600/ns 
  2. total 0 
  3. lrwxrwxrwx 1 root root 0  4月  7 22:01 ipc -> ipc:[4026531839] 
  4. lrwxrwxrwx 1 root root 0  4月  7 22:01 mnt -> mnt:[4026532520] 
  5. lrwxrwxrwx 1 root root 0  4月  7 22:01 net -> net:[4026531956] 
  6. lrwxrwxrwx 1 root root 0  4月  7 22:01 pid -> pid:[4026532522] 
  7. lrwxrwxrwx 1 root root 0  4月  7 22:01 user -> user:[4026531837] 
  8. lrwxrwxrwx 1 root root 0  4月  7 22:01 uts -> uts:[4026532521] 

我们可以看到,其中的ipc,net,user是同一个ID,而mnt,pid,uts都是不一样的。如果两个进程指向的namespace编号相同,就说明他们在同一个namespace下,否则则在不同namespace里面。

这些文件还有另一个作用,那就是,一旦这些文件被打开,只要其fd被占用着,那么就算PID所属的所有进程都已经结束,创建的namespace也会一直存在。比如:我们可以通过:mount –bind /proc/4600/ns/uts ~/uts 来hold这个namespace。

另外,我们在上篇中讲过一个setns的系统调用,其函数声明如下:


  1. intsetns(intfd,intnstype); 

其中第一个参数就是一个fd,也就是一个open()系统调用打开了上述文件后返回的fd,比如:


  1. fd = open(“/proc/4600/ns/nts”, O_RDONLY); // 获取namespace文件描述符 
  2. setns(fd, 0);// 加入新的namespace 

作者:陈皓

来源:51CTO

时间: 2024-10-09 23:57:04

Docker 基础技术:Linux Namespace(下)的相关文章

Docker基础技术:Linux Namespace(上)

时下最热的技术莫过于Docker了,很多人都觉得Docker是个新技术,其实不然,Docker除了其编程语言用go比较新外,其实它还真不是个新东西,也就是个新瓶装旧酒的东西,所谓的The New "Old Stuff".Docker和Docker衍生的东西用到了很多很酷的技术,我会用几篇 文章来把这些技术给大家做个介绍,希望通过这些文章大家可以自己打造一个山寨版的docker. 当然,文章的风格一定会尊重时下的"流行"--我们再也没有整块整块的时间去看书去专研,而我

Docker基础技术:Linux Namespace【上】

点点收获: //之前发现Coolshell上好久不更新了, 博主果然去搞大业去了,只恨这几篇文章看到太晚了啊~太厉害了. 1.  clone(), unshare(), setns()初识; 主要是š三个系统调用 šclone() - 实现线程的系统调用,用来创建一个新的进程,并可以通过设计上述参数达到隔离. šunshare() - 使某进程脱离某个namespace šsetns() - 把某进程加入到某个namespace 2.  学习了一个命令 -- ipcs -- report XSI

Linux Namespace机制简介

最近Docker技术越来越受到关注,作为Docker中很重要的一项技术,Namespace也就经常在Docker的简介里面看到. 在这里总结一下它的内部机制.也解决一下自己原来的一些疑惑. Namespace是什么: C++中的Namespace: 首先,先提一下Namespace是什么.最早知道这个名词是在学习C++语言的时候.由于现在的系统越来越复杂,代码中不同的模块就可能使用相同变量,于是就出现了Namespace,来对全局作用域进行划分. 比如C++的标注库都定义在STD Namespa

Docker应用容器基础技术:Linux Namespace 学习教程

我们开始.先从Linux Namespace开始.  简介 Linux Namespace是Linux提供的一种内核级别环境隔离的方法.不知道你是否还记得很早以前的Unix有一个叫chroot的系统调用通过修改根目录把用户jail到一个特定目录下chroot提供了一种简单的隔离模式chroot内部的文件系统无法访问外部的内容.Linux Namespace在此基础上提供了对UTS.IPC.mount.PID.network.User等的隔离机制. 举个例子我们都知道Linux下的超级父亲进程的P

Docker用以提高Linux内核安全性的三大热点技术

关于译者Ghostcloud Ghostcloud(中文名:精灵云)是成都精灵云科技有限公司旗下的基于Docker的PaaS/CaaS平台品牌.公司成立于2015年,核心团队由来自EMC.Veritas.华为.IBM.Microsoft的核心技术主管和架构师组成.精灵云作为国内首批从事容器虚拟化研发的企业,为企业级行业客户提供针对互联网化.私有云管理平台.大数据业务基础架构的平台服务,在国内Docker社区贡献排名前三.主创团队曾参与Beego开源项目研发,并主导发布<Docker容器实战:原理

《自己动手写Docker》书摘之一: Linux Namespace

Linux Namespace 介绍 我们经常听到说Docker 是一个使用了Linux Namespace 和 Cgroups 的虚拟化工具,但是什么是Linux Namespace 它在Docker内是怎么被使用的,说到这里很多人就会迷茫,下面我们就先介绍一下Linux Namespace 以及它们是如何在容器里面使用的. 概念 Linux Namespace 是kernel 的一个功能,它可以隔离一系列系统的资源,比如PID(Process ID),User ID, Network等等.一

docker和传统Linux下多用户多任务,有什么区别?

问题描述 A:docker和传统Linux下多用户多任务,有什么区别?该问题来自CSDNDocker技术交流群(303806405),由版主xinshubiao整理.更多0 解决方案 解决方案二:B:docker,取虚拟化所长,去虚拟化所短.C:主要是在互相隔离的情况下尽最大可能地节省系统资源吧(或者说共享公共资源).A:多用户多任务不是也好像是尽可能节省系统资源吗?C:可迁移性A:就这点区别么?该解答来自CSDNDocker技术交流群(303806405),由版主xinshubiao整理,由于

Tesla真正的启示:基础技术停滞不前的情况下,你该做什么?

Elon Musk被称为硅谷最有可能接过乔布斯衣钵的人.他们两人确实都有一个共同的特点:能在决定行业发展的基础技术停滞不前的情况下,产生颠覆式创新. 基础技术是极难突破的,所以,很多人认为当技术发展遇到瓶颈的时候,行业必然就停滞不前.以电动汽车为例.电动汽车不温不火存在很久了,即使这些年全球政府对环保空前重视,各种补贴各种政策利好,也没见哪款电动汽车真正火起来,原因就是电池技术的徘徊不前.但是之前一个主要混互联网圈的人物,仅仅几年的时间,就开发出一款让传统汽车巨头相形见绌的电动汽车--Tesla

Linux环境下的高级隐藏技术_unix linux

    摘要:本文深入分析了Linux环境下文件.进程及模块的高级隐藏技术,其中包括:Linux可卸载模块编程技术.修改内存映象直接对系统调用进行修改技术,通过虚拟文件系统proc隐藏特定进程的技术. 隐藏技术在计算机系统安全中应用十分广泛,尤其是在网络攻击中,当攻击者成功侵入一个系统后,有效隐藏攻击者的文件.进程及其加载的模块变得尤为重要.本文将讨论Linux系统中文件.进程及模块的高级隐藏技术,这些技术有的已经被广泛应用到各种后门或安全检测程序之中,而有一些则刚刚起步,仍然处在讨论阶段,应用