1818IP-服务器技术教程,云服务器评测推荐,服务器系统排错处理,环境搭建,攻击防护等

当前位置:首页 - 运维 - 正文

君子好学,自强不息!

在 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

$cat/proc/2465/uid_map
010001

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

$cat/proc/$$/uid_map
004294967295

另外,需要注意的是:

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

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

#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>
#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”);
return;
}
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,intoutside_id,intlen){
charfile[256];
sprintf(file,”/proc/%d/gid_map”,pid);
set_map(file,inside_id,outside_id,len);
}
intcontainer_main(void*arg)
{
printf(“Container[%5d]–insidethecontainer!\n”,getpid());
printf(“Container: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
sethostname(“container”,10);
//remount“/proc”tomakesurethe“top”and“ps”showcontainer’sinformation
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,
//weneededitthe/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/overflowgid
set_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;
}

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

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

hchen@ubuntu:~$id
uid=1000(hchen)gid=1000(hchen)groups=1000(hchen)
hchen@ubuntu:~$./user#<–以hchen用户运行
Parent:eUID=1000;eGID=1000,UID=1000,GID=1000
Parent[3262]–startacontainer!
Parent[3262]–Container[3263]!
Parent[3262]–user/groupmappingdone!
Container[1]–insidethecontainer!
Container:eUID=0;eGID=0,UID=0,GID=0#<—Container里的UID/GID都为0了
Container[1]–setuphostname!
root@container:~#id#<—-我们可以看到容器里的用户和命令行提示符是root用户了
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的虚拟网卡——给容器用的):

hchen@ubuntu:~$iplinkshow
1:lo:<LOOPBACK,UP,LOWER_UP>mtu65536qdiscnoqueuestate…
link/loopback00:00:00:00:00:00brd00:00:00:00:00:00
2:eth0:<BROADCAST,MULTICAST,UP,LOWER_UP>mtu1500qdisc…
link/ether00:0c:29:b7:67:7dbrdff:ff:ff:ff:ff:ff
3:docker0:<BROADCAST,MULTICAST,UP,LOWER_UP>mtu1500…
link/ether56:84:7a:fe:97:99brdff:ff:ff:ff:ff:ff
5:veth22a38e6:<BROADCAST,UP,LOWER_UP>mtu1500qdisc…
link/ether8e:30:2a:ac:8c:d1brdff:ff:ff:ff:ff:ff

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

##首先,我们先增加一个网桥lxcbr0,模仿docker0
brctladdbrlxcbr0
brctlstplxcbr0off
ifconfiglxcbr0192.168.10.1/24up#为网桥设置IP地址
##接下来,我们要创建一个networknamespace–ns1
#增加一个namesapce命令为ns1(使用ipnetnsadd命令)
ipnetnsaddns1
#激活namespace中的loopback,即127.0.0.1(使用ipnetnsexecns1来操作ns1中的命令)
ipnetnsexecns1iplinksetdevloup
##然后,我们需要增加一对虚拟网卡
#增加一个pair虚拟网卡,注意其中的veth类型,其中一个网卡要按进容器中
iplinkaddveth-ns1typevethpeernamelxcbr0.1
#把veth-ns1按到namespacens1中,这样容器中就会有一个新的网卡了
iplinksetveth-ns1netnsns1
#把容器里的veth-ns1改名为eth0(容器外会冲突,容器内就不会了)
ipnetnsexecns1iplinksetdevveth-ns1nameeth0
#为容器中的网卡分配一个IP地址,并激活它
ipnetnsexecns1ifconfigeth0192.168.10.11/24up
#上面我们把veth-ns1这个网卡按到了容器中,然后我们要把lxcbr0.1添加上网桥上
brctladdiflxcbr0lxcbr0.1
#为容器增加一个路由规则,让容器可以访问外面的网络
ipnetnsexecns1iprouteadddefaultvia192.168.10.1
#在/etc/netns下创建networknamespce名称为ns1的目录,
#然后为这个namespace设置resolv.conf,这样,容器内就可以访问域名了
mkdir-p/etc/netns/ns1
echo”nameserver8.8.8.8″>/etc/netns/ns1/resolv.conf

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

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

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

iplinkaddpeerAtypevethpeernamepeerB
brctladdifdocker0peerA
iplinksetpeerAup
iplinksetpeerBnetns${container-pid}
ipnetnsexec${container-pid}iplinksetdevpeerBnameeth1
ipnetnsexec${container-pid}iplinkseteth1up;
ipnetnsexec${container-pid}ipaddradd${ROUTEABLE_IP}deveth1;

上面的示例是我们为正在运行的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的程序),然后不要退出。

$sudo./pid.mnt
[sudo]passwordforhchen:
Parent[4599]–startacontainer!
Container[1]–insidethecontainer!

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

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

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

下面是父进程的:

hchen@ubuntu:~$sudols-l/proc/4599/ns
total0
lrwxrwxrwx1rootroot04月722:01ipc->ipc:[4026531839]
lrwxrwxrwx1rootroot04月722:01mnt->mnt:[4026531840]
lrwxrwxrwx1rootroot04月722:01net->net:[4026531956]
lrwxrwxrwx1rootroot04月722:01pid->pid:[4026531836]
lrwxrwxrwx1rootroot04月722:01user->user:[4026531837]
lrwxrwxrwx1rootroot04月722:01uts->uts:[4026531838]

下面是子进程的:

hchen@ubuntu:~$sudols-l/proc/4600/ns
total0
lrwxrwxrwx1rootroot04月722:01ipc->ipc:[4026531839]
lrwxrwxrwx1rootroot04月722:01mnt->mnt:[4026532520]
lrwxrwxrwx1rootroot04月722:01net->net:[4026531956]
lrwxrwxrwx1rootroot04月722:01pid->pid:[4026532522]
lrwxrwxrwx1rootroot04月722:01user->user:[4026531837]
lrwxrwxrwx1rootroot04月722:01uts->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的系统调用,其函数声明如下:

intsetns(intfd,intnstype);

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

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

本文来源:1818IP

本文地址:https://www.1818ip.com/post/8285.html

免责声明:本文由用户上传,如有侵权请联系删除!

发表评论

必填

选填

选填

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。