当前位置: 首页 > 科技观察

Docker基础技术:Linux命名空间(下)

时间:2023-03-21 01:07:17 科技观察

在Docker基础技术:Linux命名空间(上)中,我们了解了UTD、IPC、PID、Mount这四个命名空间。我们模仿了Docker做了一个很假的镜像。在这篇文章中,我主要想介绍一下LinuxUser和NetworkNamespace。好了,接下来介绍剩下的两个Namespaces。UserNamespaceUserNamespace主要使用CLONE_NEWUSER的参数。使用该参数后,内部看到的UID和GID与外部不同,默认显示为65534。那是因为容器找不到自己真实的UID,所以设置了最大的UID(其设置定义在/proc/sys/内核/overflowuid)。要把容器中的uid映射到真实系统的uid,需要修改/proc//uid_map和/proc//gid_map这两个文件。这两个文件的格式为:ID-inside-nsID-outside-nslength其中:第一个字段ID-inside-ns表示容器中显示的UID或GID,第二个字段ID-outside-ns表示容器外部映射的真实UID或GID。第三个字段表示映射的范围,一般填1,表示一一对应。例如将容器$cat/proc/2465/uid_map010001中真实的uid=1000映射为uid=0,如下例:表示将命名空间内0开始的uid映射到命名空间外0开始的uid,最大值范围是一个无符号的32位整数$cat/proc/$$/uid_map004294967295另外需要注意的是:写入这两个文件的进程需要在这个命名空间中有CAP_SETUID(CAP_SETGID)权限(见Capabilities)的writingprocessmust作为此用户命名空间的父或子用户命名空间进程。另外需要满足以下条件之一:1)父进程将有效uid/gid映射到子进程的用户命名空间,2)如果父进程有CAP_SETUID/CAP_SETGID权限,则可以映射到父进程/gid中的任何uid。这些规则看起来很烦人,我们来看程序(下面的程序有点长,但是很简单,如果你看过《Unix网络编程》第一卷,你应该能看懂):#define_GNU_SOURCE#include#include#include#include#include#include#include#include#include#include#defineSTACK_SIZE(1024*1024)staticcharcontainer_stack[STACK_SIZE];char*constcontainer_args[]={“/bin/bash”,NULL};intpipefd[2];voidset_map(char*file,intinside_id,intoutside_id,intlen){FILE*mapfd=fopen(file,"w");if(NULL==mapfd){perror("openfileerror");返回;}fprintf(mapfd,"%d%d%d",inside_id,outside_id,len);fclose(mapfd);}voidset_uid_map(pid_tpid,intinside_id,intoutside_id,intlen){charfile[256];sprintf(file,"/proc/%d/uid_map",pid);set_map(file,inside_id,outside_id,len);}voidset_gid_map(pid_tpid,intinside_id,inoutside_id,intlen){charfile[256];sprintf(文件,”/proc/%d/gid_map",pid);set_map(file,inside_id,outside_id,len);}intcontainer_main(void*arg){printf("Container[%5d]–在容器内!\n",getpid());printf(“容器:eUID=%ld;eGID=%ld,UID=%ld,GID=%ld\n”,(long)geteuid(),(long)getegid(),(long)getuid(),(long)getgid());/*等待父进程的通知再进行(进程间同步)*/charch;close(pipefd[1]);read(pipefd[0],&ch,1);printf(“Container[%5d]--setuphostname!\n",getpid());//设置主机名sethostname("容器",10);//重新挂载“/proc”以确保“top”和“ps”显示容器的信息mount("proc","/proc","proc",0,NULL);execv(container_args[0],container_args);printf("Something'swrong!\n");return1;}intmain(){constintgid=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]–startacontainer!\n",getpid());intcontainer_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);//Tomaptheuid/gid,//我们需要编辑/proc/PID/uid_map(or/proc/PID/gid_map)inparent//Thefileformatis//ID-inside-nsID-outside-nslength//ifnomapping,//theuidwillbetakenfrom/proc/sys/kernel/overflowuid//thegidwillbetakenfrom/proc/sys/kernel/overflowgidset_uid_map(container_pid,0,uid,1);set_gid_map(container_pid,0,gid,1);printf(“Parent[%5d]–user/groupmappingdone!\n”,getpid());/*通知子进程*/close(pipefd[1]);waitpid(container_pid,NULL,0);printf("Parent–containerstopped!\n");return0;}在上面的程序中,我们使用了一个管道来同步父子进程,为什么要这样做呢?因为子进程中有一个execv系统调用process,这个系统调用会覆盖掉当前子进程的所有进程空间,我们希望将用户命名空间的uid/gid映射到execv之前,这样execv运行的/bin/bash就会因为我们把uid设置为0里面的-uid变成了#的提示符整个程序运行效果如下:hchen@ubuntu:~$iduid=1000(hchen)gid=1000(hchen)groups=1000(hchen)hchen@ubuntu:~$./user#<--runParent:eUIDashchenuser=1000;eGID=1000,UID=1000,GID=1000Parent[3262]–startacontainer!Parent[3262]–Container[3263]!Parent[3262]–user/groupmappingdone!Container[1]–insidethecontainer!容器:eUID=0;eGID=0,UID=0,GID=0#<——Container中的UID/GID均为0Container[1]–setuphostname!root@container:~#id#<——我们可以在容器中看到user和命令行提示都是root用户uid=0(root)gid=0(root)groups=0(root),65534(nogroup)虽然容器是root,实际上这个容器的/bin/bash进程它作为普通用户hchen运行。这样我们容器的安全性就会提高。我们注意到UserNamespace以普通用户身份运行,但其他Namespaces需要root权限。那么,我想同时使用多个Namespace怎么办呢?一般来说,我们先创建一个普通用户的UserNamespace,然后将这个普通用户Map到root,使用root在容器中创建其他的Namespace。NetworkNamespaceNetwork的Namespace更冗长。在Linux下,我们一般使用ip命令来创建NetworkNamespace(在Docker的源码中,它并没有使用ip命令,而是自己实现了ip命令中的一些功能——它使用RawSocket发送一些“奇怪的””数据,呵呵)。这里,我还是用ip命令来说明。首先,让我们来看一张图片。下图基本上是宿主机上Docker的网络图(里面的物理网卡并不准确,因为docker可能运行在VM中,所以这里所谓的“物理网卡”其实是一个具有可路由IP的网卡,上图中Docker使用了一个私有网段,172.40.1.0,Docker也可能使用两个私有网段,10.0.0.0和192.168.0.0,关键看你是否配置在路由表,如果没有配置就会使用,如果你的路由表配置了所有私有网段,那么docker启动的时候就会报错。当你启动一个Docker容器时,你可以使用iplinkshow或ipaddrshow来查看宿主机当前的网络状态(我们可以看到有一个docker0,和一个veth22a38e6虚拟网卡——用于容器):hchen@ubuntu:~$iplinkshow1:lo:mtu65536qdiscnoqueuestate...link/loopback00:00:00:00:00:00brd00:00:00:00:00:002:eth0:mtu1500qdisc...link/ether00:0c:29:b7:67:7dbrdff:ff:ff:ff:ff:ff3:docker0:mtu1500...link/ether56:84:7a:fe:97:99brdff:ff:ff:ff:ff:ff5:veth22a38e6:mtu1500qdisc...link/ether8e:30:2a:ac:8c:d1brdff:ff:ff:ff:ff:ff那么,我们应该怎么做才能让它看起来像这样呢?我们来看一组命令:##首先,我们先添加一个网桥lxcbr0,模仿docker0brctladdbrlxcbr0brctlstplxcbr0offifconfiglxcbr0192.168.10.1/24up#为网桥设置IP地址##Connect接下来,我们要创建一个网络命名空间——ns1#添加一个namesapce命令到ns1(使用ipnetnsadd命令)ipnetnsaddns1#在namespace中激活loopback,即127.0.0.1(使用ipnetnsexecns1运行ns1中的命令)ipnetnsexecns1iplinksetdevloup##然后,我们需要添加一对虚拟网卡#添加一对虚拟网卡,注意veth类型,其中一张网卡要压入容器iplinkaddveth-ns1typevethpeernamelxcbr0.1#将veth-ns1压入namespaces1,这样容器中就会有新的网卡iplinksetveth-ns1netnsns1#将容器中的veth-ns1重命名为eth0(容器外冲突,容器内不冲突)ipnetnsexecns1iplinksetdevveth-ns1nameeth0#为容器中的网卡分配一个IP地址,并在网桥上激活brctladdiflxcbr0lxcbr0.1#为容器添加路由规则,使容器可以访问外网ipnetnsexecns1iprouteadddefaultvia192.168.10.1#创建networknamespce/etc/netns下名为ns1的目录,#然后为这个命名空间设置resolv.conf,这样在容器中就可以访问到域名了mkdir-p/etc/netns/ns1echo"nameserver8.8.8.8">/etc/netns/ns1/resolv.conf上面基本就是dockernetwork的原理了,但是,Docker的resolv.conf并没有用到这个方法,而是用了上一篇文章中MountNamespace的方法。另外docker使用进程的PID作为NetworkNamespace的名字了解这一点,您甚至可以将新网卡添加到正在运行的docker容器中:开发1;上面的例子是我们在运行的docker容器中添加一个eth1网卡,并给它一个静态的IP地址,可以让外部访问。这就需要将外部的“物理网卡”配置为混杂模式,这样eth1网卡会通过ARP协议发送自己的Mac地址,然后外部的交换机将这个IP地址的数据包转发到“物理网络”card”在网上,因为是混杂模式,eth1可以接收到相关数据,如果是自己的就接收。这样Docker容器的网络就与外界进行了通信。当然,无论是Docker的NAT方式还是混杂模式,都会有性能问题。不用说,NAT有转发开销。在混杂模式下,网卡所承受的负载将完全交给所有虚拟网卡。所以即使一张网卡上没有数据,也会受到其他网卡上数据的影响。这两种方法都不是完美的。我们知道真正解决这种网络问题需要用到VLAN技术,所以Google同学实现了Linux内核的IPVLAN驱动,基本上是为Docker量身定做的。上面的Namespace文件就是目前LinuxNamespace的玩法。现在,让我看看其他相关的东西。我们运行上一篇文章中的pid.mnt程序(即??PIDNamespace中的mountproc程序),然后不要退出。$sudo./pid.mnt[sudo]passwordforhchen:Parent[4599]–startacontainer!Container[1]–insidethecontainer!我们在另一个shell中查看父子进程的PID:hchen@ubuntu:~$pstree-p4599pid.mnt(4599)────bash(4600)我们可以去proc(/proc//ns)查看进程各个命名空间的id(内核版本需要3.8以上)。Hereistheparentprocess:hchen@ubuntu:~$sudols-l/proc/4599/nstotal0lrwxrwxrwx1rootroot04month722:01ipc->ipc:[4026531839]lrwxrwxrwx1rootroot04month722:01mnt->mnt:[4026531840]lrwxrwxrwx1root04month722:01mnt->mnt:[4026531840]lrwxrwxrwx12root04net1>net:[4026531956]lrwxrwxrwx1rootroot04Apr722:01pid->pid:[4026531836]lrwxrwxrwx1rootroot04April722:01user->user:[4026531837]lrwxrwxrwx1rootroot04Apr722:01uts->uts:1childofthefollowingprocess265:[40ubuntu:~$sudols-l/proc/4600/nstotal0lrwxrwxrwx1rootroot04月722:01ipc->ipc:[4026531839]lrwxrwxrwx1rootroot04月722:01mnt->mnt:[4026532520]lrwxrwxrwx1rootroot04月722:01net->net:[4026531956]lrwxrwxrwx1rootroot04月722:01pid->pid:[4026532522]lrwxrwxrwx1rootroot04月722:01user->user:[4026531837]lrwxrwx1rootroot04month722:01uts->uts:[4026532521]我们可以看到ipc,net,user是同一个ID,而mnt,pid,uts都是不同的。如果两个进程指向的命名空间编号相同,则说明它们在同一个命名空间,否则它们在不同的命名空间。这些文件还有一个作用,就是这些文件一旦打开,只要它们的fd被占用,即使PID所属的所有进程都结束了,创建的命名空间也会一直存在。例如:我们可以通过:mount–bind/proc/4600/ns/uts~/uts来持有这个命名空间。另外,我们在上一篇文章中讲到一个setns系统调用,它的函数声明如下:intsetns(intfd,intnstype);其中第一个参数是一个fd,它是open()系统调用,打开上面的文件并返回fd,如:fd=open(“/proc/4600/ns/nts”,O_RDONLY);//得到namespacefiledescriptorsetns(fd,0);//添加一个新的命名空间

最新推荐
猜你喜欢