寫這個(gè)系列文章主要是對之前做項(xiàng)目用到的docker相關(guān)技術(shù)做一些總結(jié),包括docker基礎(chǔ)技術(shù)Linux命名空間,cgroups,網(wǎng)絡(luò)等內(nèi)容。這是第一篇Linux命名空間,主要參考的introduction-to-linux-namespaces和
Namespaces in operation 這兩個(gè)系列博客,根據(jù)自己的理解進(jìn)行了翻譯整合。示例代碼也全部來自這兩個(gè)參考資料,為了學(xué)習(xí)方便,我建了個(gè)倉庫存放示例代碼以方便測試,代碼地址見這里,如有錯(cuò)誤,歡迎指正。
1 概述
Linux容器技術(shù)(LXC)近幾年十分流行,而其依托的技術(shù)并不是很新的東西,而是Linux內(nèi)核自帶的一套內(nèi)核級別環(huán)境隔離機(jī)制。當(dāng)然,最流行的LXC技術(shù)莫過于docker了,現(xiàn)在社區(qū)版本更名叫moby了。 Linux容器技術(shù)依賴Linux內(nèi)核的3個(gè)主要的隔離機(jī)制:chroot,cgroups,namespace。先來看看namespace,在Linux Kernel3.8以后,Linux支持6種namespace。分別是:
| namespace | 隔離內(nèi)容 | flag |
|---|---|---|
| UTS | 主機(jī)名 | CLONE_NEWUTS |
| IPC | 進(jìn)程間通信 | CLONE_NEWIPC |
| PID | chroot進(jìn)程樹 | CLONE_NEWPID |
| NS(Mount) | 掛載點(diǎn)(mount points) | CLONE_NEWNS |
| NET | 網(wǎng)絡(luò)訪問,包括接口 | CLONE_NEWNET |
| USER | 將虛擬的本地UID映射到真實(shí)的UID | CLONE_NEWUSER |
Linux內(nèi)核提供了一套API用于操作namespace實(shí)現(xiàn)環(huán)境隔離,目前namespace操作的API包括clone(), setns()以及unshare()等。通過下面的命令我們可以模擬一個(gè)類似容器的環(huán)境(隔離了PID namespace,并掛載了proc目錄),你可以發(fā)現(xiàn)只能看到bash和shell兩個(gè)進(jìn)程了,而且PID是1和2。而在原PID namespace,我們可以看到bash進(jìn)程的PID則是15011。個(gè)中緣由,且慢慢道來。
root@ubuntu:/home/vagrant/nstest# sudo unshare --fork --pid --mount-proc bash
root@ubuntu:/home/vagrant/nstest# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 29164 5876 pts/1 S 22:14 0:00 bash
root 2 0.0 0.0 24388 2592 pts/1 R+ 22:14 0:00 ps aux
root@ubuntu:/home/vagrant/nstest# ps -ef|grep unshare
root 15011 952 0 22:14 pts/1 00:00:00 unshare --fork --pid --mount-proc bash
1.1 使用clone創(chuàng)建新進(jìn)程同時(shí)創(chuàng)建namespace
代碼 ns.c 從子進(jìn)程運(yùn)行 /bin/bash,先從這個(gè)例子來看看Linux namespace的作用(為了簡單起見,略去了錯(cuò)誤檢查代碼)。
注意到在代碼中使用了clone來代替更常見的fork系統(tǒng)調(diào)用,clone實(shí)際上是Unix系統(tǒng)調(diào)用fork的一種更通用的實(shí)現(xiàn)方式,它的原型是這樣的
int clone(int (*child_func)(void *), void *child_stack, int flags, void *arg);
child_func參數(shù)為傳遞子進(jìn)程運(yùn)行的主函數(shù),如ns.c中的child_main;child_stack參數(shù)為子進(jìn)程使用的棧空間,參數(shù)flags可以指定使用的CLONE_*標(biāo)志,一次可以指定多個(gè)flag;而args則是子進(jìn)程的參數(shù)。編譯運(yùn)行上面的代碼,結(jié)果如下所示,運(yùn)行正常,但是我們很難區(qū)分這是在子進(jìn)程運(yùn)行的/bin/bash還是本身的/bin/bash。
root@ubuntu:/home/vagrant/nstest# gcc -Wall ns.c -o ns && ./ns
- Hello ?
- World !
root@ubuntu:/home/vagrant/nstest# #inside container
root@ubuntu:/home/vagrant/nstest# exit
root@ubuntu:/home/vagrant/nstest# #outside container
于是,CLONE_NEWUTS可以派上用場了。UTS namespace提供了主機(jī)名和域名的隔離,這樣每個(gè)容器就有獨(dú)立的主機(jī)名和域名,從而可以在網(wǎng)絡(luò)上被當(dāng)作一個(gè)獨(dú)立的節(jié)點(diǎn)而不是宿主機(jī)的一個(gè)進(jìn)程。修改clone函數(shù)這行代碼,加入CLONE_NEWUTS的flag,然后在子進(jìn)程中調(diào)用sethostname函數(shù),修改后代碼 ns_uts.c。
以root身份運(yùn)行它
root@ubuntu:/home/vagrant/nstest# gcc -Wall ns_uts.c -o ns_uts && ./ns_uts
- Hello ?
- World !
root@In Namespace:/home/vagrant/nstest# #inside container
root@In Namespace:/home/vagrant/nstest# exit
root@ubuntu:/home/vagrant/nstest# #outside container
可以看到,在子進(jìn)程中hostname變成了In Namespace,而父進(jìn)程的hostname為ubuntu不受子進(jìn)程修改hostname的影響,通過CLONE_NEWUTS實(shí)現(xiàn)了主機(jī)名的隔離。注意,如果不加CLONE_NEWUTS標(biāo)記運(yùn)行,會(huì)發(fā)現(xiàn)退出子進(jìn)程后hostname也還原了,這是因?yàn)閎ash只在登錄的時(shí)候讀取一次UTS,等你重新登陸就會(huì)發(fā)現(xiàn)hostname變了。因此,為了hostname隔離,加上CLONE_NEWUTS標(biāo)志。
docker容器的hostname也是通過該機(jī)制實(shí)現(xiàn)的隔離,每個(gè)容器都有自己的hostname(默認(rèn)是容器ID),并不會(huì)對宿主機(jī)的hostname產(chǎn)生任何影響。
root@ubuntu:/home/ssj# docker exec -it ssjtestnew /bin/bash
root@c9df3369e321:/# hostname
c9df3369e321
1.2 /proc/PID/ns文件
從/proc/PID/ns目錄中,我們可以看到一個(gè)進(jìn)程的namespace。比如我們運(yùn)行上面的 ./ns_uts,并查看父子進(jìn)程的namespace,結(jié)果如下,可以看到ns_uts和子進(jìn)程bash的ns目錄中,除了UTS namespace是不一樣的,表明這兩個(gè)進(jìn)程在不同的UTS名字空間,其他5個(gè)namespace是相同的。/proc/PID/ns目錄中的為符號鏈接,指向的是對應(yīng)namespace的名字,名字命名規(guī)則是namespace類型+inode數(shù)字,如ipc:[4026531839]。
root@ubuntu:/home/vagrant# ps -ef
root 3086 2741 0 02:46 pts/0 00:00:00 ./ns_uts
root 3087 3086 0 02:46 pts/0 00:00:00 /bin/bash
root@ubuntu:/home/vagrant# ls -ls /proc/3086/ns/
total 0
0 lrwxrwxrwx 1 root root 0 Aug 16 02:47 ipc -> ipc:[4026531839]
0 lrwxrwxrwx 1 root root 0 Aug 16 02:47 mnt -> mnt:[4026531840]
0 lrwxrwxrwx 1 root root 0 Aug 16 02:47 net -> net:[4026531956]
0 lrwxrwxrwx 1 root root 0 Aug 16 02:47 pid -> pid:[4026531836]
0 lrwxrwxrwx 1 root root 0 Aug 16 02:47 user -> user:[4026531837]
0 lrwxrwxrwx 1 root root 0 Aug 16 02:47 uts -> uts:[4026531838]
root@ubuntu:/home/vagrant# ls -ls /proc/3087/ns/
total 0
0 lrwxrwxrwx 1 root root 0 Aug 16 02:47 ipc -> ipc:[4026531839]
0 lrwxrwxrwx 1 root root 0 Aug 16 02:47 mnt -> mnt:[4026531840]
0 lrwxrwxrwx 1 root root 0 Aug 16 02:47 net -> net:[4026531956]
0 lrwxrwxrwx 1 root root 0 Aug 16 02:47 pid -> pid:[4026531836]
0 lrwxrwxrwx 1 root root 0 Aug 16 02:47 user -> user:[4026531837]
0 lrwxrwxrwx 1 root root 0 Aug 16 02:47 uts -> uts:[4026532182]
root@ubuntu:/home/vagrant# readlink /proc/3086/ns/uts # show parent UTS namespace
uts:[4026531838]
root@ubuntu:/home/vagrant# readlink /proc/3087/ns/uts # show child UTS namespace
uts:[4026532182]
root@ubuntu:/home/vagrant# touch ~/uts
root@ubuntu:/home/vagrant# mount --bind /proc/3087/ns/uts ~/uts
當(dāng)然namespace還有其他的用處,只要namespace的文件描述符是打開的,即便該namespace所有進(jìn)程都終止了,該namespace還是依舊存在。我們?nèi)绻苯油顺龀绦?,可以看到進(jìn)程退出后/proc/PID目錄會(huì)整個(gè)刪掉,包括ns目錄。于是,為了保存子進(jìn)程的UTS namespace,我們用mount命令先掛載該namespace,稍后我們會(huì)用setns()將進(jìn)程加入到該UTS namespace。
mount --bind /proc/3087/ns/uts ~/uts
1.3 加入已經(jīng)存在的namespace:setns()
通過setns和execve可以讓一個(gè)進(jìn)程加入一個(gè)已經(jīng)存在的namespace并在那個(gè)namespace執(zhí)行命令。測試代碼 ns_setns.c,這里用到上一節(jié)中保留的UTS namespace。
運(yùn)行結(jié)果如下:
root@ubuntu:/home/vagrant/nstest# gcc -o ns_setns ns_setns.c
root@ubuntu:/home/vagrant/nstest# ./ns_setns ~/uts /bin/bash
root@In Namespace:/home/vagrant/nstest# echo $$ ## show pid
3375
root@In Namespace:/home/vagrant/nstest# hostname
In Namespace
root@In Namespace:/home/vagrant/nstest# readlink /proc/3375/ns/
ipc mnt net pid user uts
root@In Namespace:/home/vagrant/nstest# readlink /proc/3375/ns/uts
uts:[4026532182]
可以看到該進(jìn)程的UTS namespace為我們指定的之前保留的child process的UTS namespace。
1.4 隔離一個(gè)namespace:unshare()
unshare函數(shù)可以讓進(jìn)程脫離一個(gè)namespace,它與clone類似,不同的是,unshare不需要?jiǎng)?chuàng)建新的進(jìn)程,而是在當(dāng)前進(jìn)程直接隔離namespace。
測試代碼 ns_unshare.c ,運(yùn)行之,在參數(shù)中我們傳遞-m用來隔離NS namespace(即掛載點(diǎn)的namespace),結(jié)果可以看到在新的NS namespace的shell進(jìn)程中umount了一個(gè)目錄/run/lock,并不影響老的shell進(jìn)程的掛載點(diǎn)。
root@ubuntu:/home/vagrant/nstest# echo $$ #Show pid of shell
4434
root@ubuntu:/home/vagrant/nstest# readlink /proc/4434/ns/mnt # Show shell NS namespace id
mnt:[4026532183]
root@ubuntu:/home/vagrant/nstest# cat /proc/4434/mounts|grep '/run/lock'
none /run/lock tmpfs rw,nosuid,nodev,noexec,relatime,size=5120k 0 0
root@ubuntu:/home/vagrant/nstest# ./ns_unshare -m /bin/bash #Start new shell in separate mount namespace
hello, pid=4927
root@ubuntu:/home/vagrant/nstest# echo $$
4927
root@ubuntu:/home/vagrant/nstest# readlink /proc/4927/ns/mnt #Show mount namespace ID in new shell
mnt:[4026532184]
root@ubuntu:/home/vagrant/nstest# cat /proc/4927/mounts|grep 'run/lock'
none /run/lock tmpfs rw,nosuid,nodev,noexec,relatime,size=5120k 0 0
root@ubuntu:/home/vagrant/nstest# umount /run/lock #Umount dir in separate mount namespace
root@ubuntu:/home/vagrant/nstest# cat /proc/4927/mounts|grep 'run/lock'
root@ubuntu:/home/vagrant/nstest# exit
root@ubuntu:/home/vagrant/nstest# cat /proc/4434/mounts|grep '/run/lock' #Old shell not affected
none /run/lock tmpfs rw,nosuid,nodev,noexec,relatime,size=5120k 0 0
至此,namespace操作的相關(guān)API函數(shù)已經(jīng)都說完了,接下來分別看看這6個(gè)namespace。
2 UTS Namespace
UTS是實(shí)現(xiàn)主機(jī)名和域名的隔離,在第一節(jié)中已經(jīng)說過,這里不再贅述。
3 IPC Namespace
IPC指Unix/Linux下進(jìn)程間通信的方式,可以通過共享內(nèi)存,信號量,消息隊(duì)列,管道等方法實(shí)現(xiàn)。這里我們要隔離IPC namespace,實(shí)現(xiàn)方式也很簡單,在clone函數(shù)的flags參數(shù)中加入CLONE_NEWIPC即可,這樣你可以在新的namespace中創(chuàng)建IPC,甚至是命名一個(gè),并不會(huì)有與其他應(yīng)用沖突的風(fēng)險(xiǎn)。
我們在最初的實(shí)例代碼ns.c中修改一下,加入CLONE_NEWIPC的flag。修改的代碼只有一行,如下。當(dāng)然這里的CLONE_NEWUTS不是必須的,保留這個(gè)flag只是為了更加方便的顯示效果。
/*
ns_ipc.c: used to test ipc
*/
[...]
int child_pid = clone(child_main, child_stack+STACK_SIZE,
CLONE_NEWUTS | CLONE_NEWIPC| SIGCHLD, NULL);
[...]
先通過ipcmk -Q創(chuàng)建一個(gè)IPC隊(duì)列,隊(duì)列ID為65536,然后運(yùn)行./ns_ipc,可以看到在新的namespace中并沒有該IPC隊(duì)列,做到了IPC隔離。
root@ubuntu:/home/vagrant/nstest# ipcmk -Q
Message queue id: 65536
root@ubuntu:/home/vagrant/nstest# ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
0x0a3817cf 65536 root 644 0 0
root@ubuntu:/home/vagrant/nstest# ./ns_ipc
- Hello ?
- World !
root@In Namespace:/home/vagrant/nstest# ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
root@In Namespace:/home/vagrant/nstest# exit
root@ubuntu:/home/vagrant/nstest# ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
0x0a3817cf 65536 root 644 0 0
接下來可能有人要問了,那這種父子進(jìn)程在不同的IPC namespace了,它們之間怎么通信呢?前面說過,進(jìn)程間通信有信號量,共享內(nèi)存,管道,F(xiàn)IFO,sockets等。由于上下文的改變,使用信號量也許不是最佳方案。而使用共享內(nèi)存則有效率上的問題,如果不隔離網(wǎng)絡(luò)棧的話也可以用sockets,但是我們現(xiàn)在要一步步隔離一切,因此sockets也不合適。FIFO則可以用于任意進(jìn)程間的通信,F(xiàn)IFO是一種特殊的文件類型,在文件系統(tǒng)中是有對應(yīng)路徑的,它的問題也與sockets類似,因?yàn)槲覀円綦x文件系統(tǒng)的話,它也不合適。管道用于有親屬關(guān)系的進(jìn)程之間通信,比如父子進(jìn)程或者兄弟進(jìn)程之間通信,很適合不同namespace的進(jìn)程通信。
使用管道實(shí)現(xiàn)不同namespace之間進(jìn)程通信的示例代碼 ns_ipc.c,運(yùn)行之,可以看到位于不同namespace的父子進(jìn)程確實(shí)通信成功了。
root@ubuntu:/home/vagrant/nstest# ./ns_ipc
- Hello ?
- World !
root@In Namespace:/home/vagrant/nstest# exit
root@ubuntu:/home/vagrant/nstest#
4 PID Namespace
實(shí)現(xiàn)PID隔離加上CLONE_NEWPID標(biāo)識(shí)即可。示例代碼 ns_pid.c ,運(yùn)行之:
root@ubuntu:/home/vagrant/nstest# ./ns_pid
- [ 7627] Hello ?
- [ 1] World !
root@In Namespace:/home/vagrant/nstest# echo $$ ##In new PID namespace
1
root@In Namespace:/home/vagrant/nstest# kill -KILL 7627
bash: kill: (7627) - No such process
###host ps view
root@ubuntu:/home/vagrant/nstest# ps -ef|grep 7627
root 7627 2768 0 04:40 pts/1 00:00:00 ./pid
root 7628 7627 0 04:40 pts/1 00:00:00 /bin/bash
可以看到在不同PID namespace中運(yùn)行的/bin/bash的PID為1。而它的父進(jìn)程的PID是7627。而在父進(jìn)程namespace中,可以看到/bin/bash的進(jìn)程為7268。如果你試圖在新的Namespace中去kill某個(gè)不同namespace中的進(jìn)程,則會(huì)報(bào)錯(cuò)提示進(jìn)程不存在,達(dá)到了進(jìn)程隔離的目的。
要注意的是,這個(gè)時(shí)候你在新的namespace中用ps,top等命令去查看,會(huì)發(fā)現(xiàn)7627這個(gè)進(jìn)程是可見的。這與我們在docker容器中看到的不一致,如在我創(chuàng)建的一個(gè)redis容器中,用ps, top其實(shí)是只看得到容器所在namespace的進(jìn)程的。
root@ubuntu:/home/ssj# docker exec -it redistest /bin/bash
root@0b86fb961783:/data# ps -ef
UID PID PPID C STIME TTY TIME CMD
redis 1 0 0 02:44 ? 00:00:00 redis-server *:6379
root 18 0 0 02:45 ? 00:00:00 /bin/bash
root 25 18 0 02:45 ? 00:00:00 ps -ef
這是因?yàn)閜s命令讀取的是/proc文件系統(tǒng)獲取的信息,而文件系統(tǒng)我們還沒有隔離,所以在新的namespace中可以看到所有的進(jìn)程,接下來我們會(huì)用NS namespace來實(shí)現(xiàn)這個(gè)隔離。
5 NS Namespace
NS namespace也就是掛載點(diǎn)相關(guān)的了,在第4節(jié)的代碼基礎(chǔ)上加入CLONE_NEWNS的flag,并在子進(jìn)程掛載 /proc目錄。修改后創(chuàng)建進(jìn)程的代碼 ns_ns.c, 運(yùn)行之:
root@ubuntu:/home/vagrant/nstest# ./ns_ns
- [27137] Hello ?
- [ 1] World !
root@In Namespace:/home/vagrant/nstest# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 20:37 pts/0 00:00:00 /bin/bash
root 3 1 0 20:39 pts/0 00:00:00 ps -ef
root@In Namespace:/home/vagrant/nstest# ls /proc/
1 bus cpuinfo dma filesystems ioports kcore kpagecount meminfo mpt partitions softirqs sysrq-trigger tty vmstat
5 cgroups crypto driver fs ipmi keys kpageflags misc mtrr sched_debug stat sysvipc uptime zoneinfo
acpi cmdline devices execdomains interrupts irq key-users loadavg modules net self swaps timer_list version
buddyinfo consoles diskstats fb iomem kallsyms kmsg locks mounts pagetypeinfo slabinfo sys timer_stats vmallocinfo
可以看到ps命令確實(shí)只顯示了當(dāng)前namespace下面的進(jìn)程了,而且ls /proc/命令查看發(fā)現(xiàn)/proc目錄下面的內(nèi)容也清爽多了。docker使用NS namespace實(shí)現(xiàn)了一些文件系統(tǒng)的掛載,原理與這個(gè)類似,結(jié)合chdir和chroot可以實(shí)現(xiàn)一個(gè)山寨的docker鏡像。
這個(gè)時(shí)候我們再來看看docker中PID和NS namespace具體的實(shí)現(xiàn)(我的docker版本是1.13.1,其他版本可能有所不同),我這里在宿主機(jī)起了一個(gè)redis容器名為redistest,通過pstree可以看到進(jìn)程關(guān)系如下:
|-dockerd-+-docker-containerd-+-docker-containerd-shim-+-redis-server---3*[{redis-server}]
| | | `-8*[{docker-containe}]
| | `-12*[{docker-containe}]
| `-19*[{dockerd}]
這里對應(yīng)進(jìn)程關(guān)系就是:
- dockerd進(jìn)程創(chuàng)建了一個(gè)docker-containerd子進(jìn)程,而docker-contianerd子進(jìn)程再創(chuàng)建子進(jìn)程docker-containerd-shim,也就是對應(yīng)具體容器的進(jìn)程。
- 容器進(jìn)程docker-containerd-shim創(chuàng)建容器里面的1號進(jìn)程redis-server。
- 通過查看
/proc/PID/ns目錄就可以發(fā)現(xiàn),dockerd,dockerd-containerd以及dockerd-containerd-shim的namespace都是一樣的,而容器里面的1號進(jìn)程 redis-server的namespace除了User namespace外,其他的namespace都已經(jīng)不同。也就是說,從容器里面的1號進(jìn)程開始,進(jìn)程的namespace開始隔離。 -
另外注意一點(diǎn)的是,當(dāng)你使用
docker exec -it redistest /bin/bash命令進(jìn)入容器的時(shí)候,這個(gè)/bin/bash進(jìn)程的父進(jìn)程其實(shí)是另外一個(gè) docker-containerd-shim進(jìn)程,只是/bin/bash進(jìn)程的namespace和redis-server進(jìn)程一樣,所以這個(gè)時(shí)候你在redistest容器中ps -ef,可以看到除了redis-server進(jìn)程外,還有/bin/bash進(jìn)程。通過exec命令進(jìn)入容器后,再來看進(jìn)程關(guān)系,是下面這樣的:
|-dockerd-+-docker-containerd-+-docker-containerd-shim-+-redis-server---3*[{redis-server}]
| | | `-8*[{docker-containe}]
| | |-docker-containerd-shim-+-bash
| | | `-8*[{docker-containe}]
| | `-12*[{docker-containe}]
| `-19*[{dockerd}]
而在容器在自己的NS namespace中掛載了很多目錄,如下面這些:
/dev/sda8 on /etc/resolv.conf type ext4 (rw,relatime,data=ordered)
/dev/sda8 on /etc/hostname type ext4 (rw,relatime,data=ordered)
/dev/sda8 on /etc/hosts type ext4 (rw,relatime,data=ordered)
...
proc on /proc/irq type proc (ro,nosuid,nodev,noexec,relatime)
proc on /proc/sys type proc (ro,nosuid,nodev,noexec,relatime)
6 NET Namespace
NET namespace是指網(wǎng)絡(luò)上的隔離,通過加入CLONE_NEWNET來實(shí)現(xiàn)。在討論這個(gè)之前,可以先看看通過ip命令如何手動(dòng)創(chuàng)建network namespace以及veth設(shè)備等。veth主要的目的是為了跨NET namespace之間提供一種類似于Linux進(jìn)程間通信的技術(shù),所以veth總是成對出現(xiàn),如下面的veth0和veth1。它們位于不同的NET namespace中,在veth設(shè)備任意一端接收到的數(shù)據(jù),都會(huì)從另一端發(fā)送出去。veth工作在L2數(shù)據(jù)鏈路層,只負(fù)責(zé)數(shù)據(jù)傳輸,不會(huì)更改數(shù)據(jù)包。
# Create a "demo" namespace
ip netns add demo
# create a "veth" pair
ip link add veth0 type veth peer name veth1
# and move one to the namespace
ip link set veth1 netns demo
# configure the interfaces (up + IP)
ip netns exec demo ip link set lo up
ip netns exec demo ip link set veth1 up
ip netns exec demo ip addr add 169.254.1.2/30 dev veth1
ip link set veth0 up
ip addr add 169.254.1.1/30 dev veth0
執(zhí)行完成后,我們可以在宿主機(jī)里面看到網(wǎng)絡(luò)設(shè)備是這樣的:
root@ubuntu:/home/vagrant# ip -d link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 promiscuity 0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether 08:00:27:ec:df:9c brd ff:ff:ff:ff:ff:ff promiscuity 0
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether 08:00:27:57:25:68 brd ff:ff:ff:ff:ff:ff promiscuity 0
5: veth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether 62:14:fd:45:f8:0e brd ff:ff:ff:ff:ff:ff promiscuity 0
veth
root@ubuntu:/home/vagrant# ethtool -S veth0
NIC statistics:
peer_ifindex: 4
而在demo這個(gè)NET namespace中,看到的網(wǎng)絡(luò)設(shè)備是這樣的:
root@ubuntu:/home/vagrant# ip netns exec demo ip -d link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 promiscuity 0
4: veth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether 6a:7d:49:3f:bc:8e brd ff:ff:ff:ff:ff:ff promiscuity 0
veth
這個(gè)原理就是先創(chuàng)建一個(gè)新的NET namespace名為demo,然后創(chuàng)建一對veth設(shè)備,veth0和veth1,接著將veth1移動(dòng)到namespace demo,而veth0仍然保留在原來的namespace,然后啟動(dòng)對應(yīng)的veth設(shè)備。這樣一對veth設(shè)備分屬于不同的namespace,并可以通信。然后給veth0和veth1設(shè)置ip并啟動(dòng)它們。要查看veth的一對設(shè)備中另外一個(gè),可以用 ethtool -S命令。實(shí)現(xiàn)上面功能的代碼 ns_net.c ,運(yùn)行之,如下:
root@ubuntu:/home/vagrant/nstest# ./ns_net
- [ 2760] Hello ?
- [ 1] World !
### 宿主機(jī)namespace
root@ubuntu:/home/vagrant/nstest# ip -d link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 promiscuity 0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether 08:00:27:ec:df:9c brd ff:ff:ff:ff:ff:ff promiscuity 0
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether 08:00:27:57:25:68 brd ff:ff:ff:ff:ff:ff promiscuity 0
11: veth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether ce:95:ad:9e:ee:6b brd ff:ff:ff:ff:ff:ff promiscuity 0
veth
### 新的namespace
root@In Namespace:/home/vagrant/nstest# ip -d link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 promiscuity 0
10: veth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether 9a:e9:95:53:c3:28 brd ff:ff:ff:ff:ff:ff promiscuity 0
veth
root@In Namespace:/home/vagrant/nstest# ethtool -S veth1
NIC statistics:
peer_ifindex: 11
docker網(wǎng)絡(luò)分為bridge, host, overlay等幾種類型。host就是與主機(jī)共用namespace,這里不單獨(dú)分析了。而bridge就與前面例子中類似,不同的是僅僅有veth容器還無法與外部聯(lián)通,因此docker借助了網(wǎng)橋技術(shù)用于連接不同網(wǎng)段,在L2層進(jìn)行數(shù)據(jù)轉(zhuǎn)發(fā),將veth0加入到宿主機(jī)的網(wǎng)橋docker0中,并在iptables加入對應(yīng)的NAT規(guī)則,以保證容器可以與外部連通。注意docker中NET namespace的隔離不是通過ip命令實(shí)現(xiàn)的(因?yàn)椴皇撬械膬?nèi)核版本都有ip netns這個(gè)高級命令),而是通過netlink基于操作系統(tǒng)調(diào)用的方式實(shí)現(xiàn)的。而overlay網(wǎng)絡(luò)則是通過vxlan協(xié)議實(shí)現(xiàn),對應(yīng)的veth會(huì)橋接到overlay的NET namespace一個(gè)br0網(wǎng)橋上。bridge和overlay網(wǎng)絡(luò)的一個(gè)示意圖如下(圖來自 deep-dive-into-docker-overlay-networks),其中192.168.0.X這個(gè)是自定義的overlay網(wǎng)絡(luò),而172.18.0.X的則是bridge網(wǎng)絡(luò),docker網(wǎng)絡(luò)部分會(huì)在下一篇文章再詳細(xì)分析。

7 USER Namespace
7.1 創(chuàng)建新的USER Namespace
加上 CLONE_NEWUSER flag可以實(shí)現(xiàn)USER namespace的隔離。示例如下(注意,在debian或者ubuntu中必須設(shè)置/proc/sys/kernel/unprivileged_userns_clone這個(gè)文件值為1,否則無法以普通用戶運(yùn)行帶CLONE_NEWUSER標(biāo)記的clone命令
)
示例代碼 ns_user.c,以普通用戶運(yùn)行之:
vagrant@ubuntu:~/nstest$ id -u
1000
vagrant@ubuntu:~/nstest$ id -g
1000
vagrant@ubuntu:~/nstest$ gcc -o ns_user ns_user.c -lcap
#如果編譯報(bào)錯(cuò)的話,安裝libcap-dev模塊,sudo apt-get install libcap-dev
vagrant@ubuntu:~/nstest$ ./user
eUID = 65534; eGID = 65534; capabilities: = cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend+ep
這里有幾點(diǎn)注意的:
-
其一,從capabilities輸出可以看到子進(jìn)程在它的namespace里面有全部的capability,雖然我們是用普通用戶權(quán)限運(yùn)行的程序。當(dāng)一個(gè)新的USER namespace創(chuàng)建的時(shí)候,這個(gè)namespace的第一個(gè)進(jìn)程就被賦予了全部的capability。capability是為了實(shí)現(xiàn)更精細(xì)化的權(quán)限控制而加入的。(我們以前熟知通過設(shè)置文件的SUID位,這樣非root用戶的可執(zhí)行文件運(yùn)行后的euid會(huì)成為文件的擁有者ID,比如passwd命令運(yùn)行起來后有root權(quán)限。一旦SUID的文件存在漏洞,便可能被利用而增加安全風(fēng)險(xiǎn))。查看文件的capability的命令為
filecap -a,而查看進(jìn)程capability的命令為pscap -a(pscap和filecap工具需要安裝libcap-ng-utils這個(gè)包)。對capability的那串?dāng)?shù)字解碼命令為capsh --decode=00000000000000c0。更多capability的內(nèi)容見參考資料4。對于capability,可以看一個(gè)簡單的例子便于理解。如ubuntu14.04系統(tǒng)中自帶的ping工具,它是有設(shè)置SUID位的。這里拷貝ping到我的用戶目錄下名為anotherping,可以看到它的SUID位是沒有了的,運(yùn)行anotherping,會(huì)提示權(quán)限錯(cuò)誤。這里,我們只要將其加上
cap_net_raw權(quán)限即可,不需要設(shè)置SUID位那么大的權(quán)限。vagrant@ubuntu:~$ ls -ls /bin/ping 44 -rwsr-xr-x 1 root root 44168 May 7 2014 /bin/ping vagrant@ubuntu:~$ cp /bin/ping anotherping vagrant@ubuntu:~$ ls -ls anotherping 44 -rwxr-xr-x 1 vagrant vagrant 44168 Aug 27 03:27 anotherping vagrant@ubuntu:~$ ping -c1 www.163.com PING 163.xdwscache.ourglb0.com (112.90.246.87) 56(84) bytes of data. 64 bytes from ns.local (112.90.246.87): icmp_seq=1 ttl=63 time=11.9 ms ... vagrant@ubuntu:~$ ./anotherping -c1 www.163.com ping: icmp open socket: Operation not permitted vagrant@ubuntu:~$ sudo setcap cap_net_raw+ep ./anotherping vagrant@ubuntu:~$ ./anotherping -c1 www.163.com PING 163.xdwscache.ourglb0.com (112.90.246.87) 56(84) bytes of data. 64 bytes from ns.local (112.90.246.87): icmp_seq=1 ttl=63 time=12.4 ms ... 其二,一個(gè)進(jìn)程的uid和gid在不同的USER namespace是可以不一樣的,這需要一個(gè)namespace內(nèi)部映射到namespace外部的映射關(guān)系。這樣當(dāng)一個(gè)USER namespace中的進(jìn)程的操作可能影響到外部系統(tǒng)時(shí),可以對這個(gè)進(jìn)程的權(quán)限進(jìn)行檢查。如果一個(gè)用戶ID在USER namespace中沒有映射關(guān)系,則
getuid()系統(tǒng)調(diào)用會(huì)返回/proc/sys/kernel/overflowuid值作為用戶ID,這個(gè)值默認(rèn)為65534,就如我們前面程序中輸出一樣(gid對應(yīng)的文件名為overflowgid)。其三,盡管通過clone系統(tǒng)調(diào)用創(chuàng)建的子進(jìn)程在新的USER namespace中有所有權(quán)限,但是它在
parent user namespace是沒有任何權(quán)限的,即便以root身份運(yùn)行也是一樣。user namespace的創(chuàng)建可以是嵌套的,一個(gè)user namespace一定有個(gè)parent user namespace,可以有零或者多個(gè)child user namespace。子進(jìn)程的parent user namespace就是調(diào)用clone()或者unshare()通過CLONE_NEWUSER的flag創(chuàng)建新namespace的那個(gè)父進(jìn)程的user namespace。
7.2 映射uid和gid
創(chuàng)建新的user namespace之后第一步就是設(shè)置好user和group的映射關(guān)系。這個(gè)映射通過設(shè)置/proc/PID/uid_map(gid_map)實(shí)現(xiàn),格式如下:
ID-inside-ns ID-outside-ns length
不是所有的進(jìn)程都能隨便修改映射文件的,必須同時(shí)具備如下條件:
- 修改映射文件的進(jìn)程必須有PID進(jìn)程所在user namespace的
CAP_SETUID/CAP_SETGID權(quán)限。進(jìn)程的capability一般是通過其可執(zhí)行文件的capability獲得。 - 修改映射文件的進(jìn)程必須是跟PID在同一個(gè)user namespace或者PID的parent user namespace。
- 映射文件
uid_map和gid_map只能寫入一次,再次寫入會(huì)報(bào)錯(cuò)。
下面來測試下7.1中的例子:
#在第一個(gè)終端運(yùn)行 ns_user
vagrant@ubuntu:~/nstest$ ./ns_user x
eUID = 65534; eGID = 65534; capabilities: = ...ep
#在第二個(gè)終端寫入該進(jìn)程對應(yīng)的uid_map
vagrant@ubuntu:~/nstest$ ps -C ns_user -o 'pid ppid uid comm'
PID PPID UID COMMAND
8775 8577 1000 ns_user
8776 8775 1000 ns_user
vagrant@ubuntu:~/nstest$ echo '0 1000 1' > /proc/8776/uid_map
#第一個(gè)終端此時(shí)輸出為:
vagrant@ubuntu:~/nstest$ ./ns_user x
eUID = 0; eGID = 65534; capabilities: = ...ep
#在第二個(gè)終端繼續(xù)寫入gid_map
vagrant@ubuntu:~/nstest$ echo '0 1000 1' > /proc/8776/gid_map
#第一個(gè)終端此時(shí)輸出為:
vagrant@ubuntu:~/nstest$ ./ns_user x
eUID = 0; eGID = 0; capabilities: = ...ep
可以看到,我們在位于parent user namespace的bash進(jìn)程中通過echo命令修改uid_map和gid_map都是可以成功的。這是因?yàn)槲业臏y試環(huán)境的bash進(jìn)程具有CAP_SETUID和CAP_SETGID權(quán)限的,查看/proc/PID/status可以驗(yàn)證進(jìn)程的權(quán)限或者getcap可以驗(yàn)證一個(gè)可執(zhí)行文件的權(quán)限,如下驗(yàn)證bash的權(quán)限,如果bash原來沒有這兩個(gè)權(quán)限,可以通過命令sudo setcap cap_setgid,cap_setuid+ep /bin/bash設(shè)置:
vagrant@ubuntu:~/nstest$ cat /proc/$$/status | egrep 'Cap(Inh|Prm|Eff)'
CapInh: 0000000000000000
CapPrm: 00000000000000c0
CapEff: 00000000000000c0
vagrant@ubuntu:~/nstest$ getcap /bin/bash
/bin/bash = cap_setgid,cap_setuid+ep
這里有個(gè)要注意的地方,ubuntu14.04的/bin/bash文件默認(rèn)就有修改新的user namespace進(jìn)程的uid_map的權(quán)限,如果要修改gid_map要另外加下cap_setgid權(quán)限。而其他的可執(zhí)行文件,默認(rèn)也是只有cap_setuid權(quán)限,比如網(wǎng)上很多文章中提到的一個(gè)設(shè)置user namespace的例子,在ubuntu14.04里面設(shè)置gid_map會(huì)失敗,因?yàn)榭蓤?zhí)行文件沒有cap_setgid權(quán)限,需要加上gid權(quán)限才能成功修改gid_map。
看這個(gè)例子,代碼 ns_child_exec.c,執(zhí)行后可以發(fā)現(xiàn)在新的user namespace里面的bash里面通過echo命令設(shè)置uid_map和gid_map都會(huì)失敗,這是因?yàn)楫?dāng)一個(gè)非root用戶的進(jìn)程執(zhí)行execve()時(shí),進(jìn)程的capability會(huì)被清空。于是,子進(jìn)程雖然有新的user namespace所有的權(quán)限集合,但是通過它exevce執(zhí)行的bash進(jìn)程以及bash進(jìn)程的子進(jìn)程是沒有對應(yīng)的capability的。
vagrant@ubuntu:~/nstest$ ./ns_child_exec -U bash
nobody@ubuntu:~/nstest$ id -u #新的user namespace運(yùn)行的bash進(jìn)程
65534
nobody@ubuntu:~/nstest$ id -g
65534
nobody@ubuntu:~/nstest$ echo '0 1000 1' > /proc/$$/uid_map
bash: echo: write error: Operation not permitted
nobody@ubuntu:~/nstest$ echo '0 1000 1' > /proc/$$/gid_map
bash: echo: write error: Operation not permitted
為了設(shè)置映射文件,因此需要在父進(jìn)程中設(shè)置,示例代碼 userns_child_exec.c。注意一點(diǎn)的是,要在userns_child_exec進(jìn)程中成功設(shè)置gid_map文件,需要給可執(zhí)行文件加上 cap_setgid權(quán)限,此外,還要保證 /bin/bash是有cap_setgid權(quán)限的:
root@ubuntu:~/nstest# setcap cap_setgid+ep ./userns_child_exec
vagrant@ubuntu:~/nstest$ ./userns_child_exec -U -M '0 1000 1' -G '0 1000 1' bash
root@ubuntu:~/nstest# id -u # 新的user namespace
0
root@ubuntu:~/nstest# id -g
0
root@ubuntu:~/nstest# cat /proc/$$/status | egrep 'Cap(Inh|Prm|Eff)'
CapInh: 0000000000000000
CapPrm: 0000003fffffffff
CapEff: 0000003fffffffff
最后一點(diǎn)要注意的是,uid_map文件里面的 ID-outside-ns 這個(gè)值是根據(jù)當(dāng)前讀取文件的user namespace生成的,這個(gè)是什么意思呢?看下面的例子就明白了。在兩個(gè)終端里面分別運(yùn)行 userns_child_exec程序,設(shè)置不同的ID-inside-ns,運(yùn)行結(jié)果如下所示。也就是說,我們在初始的user namespace創(chuàng)建了2個(gè)child user namespace,一個(gè)是映射的uid為0,另一個(gè)映射的為200,在第一個(gè)終端看第二個(gè)終端進(jìn)程對應(yīng)的映射關(guān)系時(shí)可以發(fā)現(xiàn)uid_map值為 200 0 1,也就是說第二個(gè)user namespace中的進(jìn)程用戶ID映射到了當(dāng)前user namespace的uid 0,而不是初始的user namespace的1000。從第二個(gè)終端里面看第一個(gè)終端的進(jìn)程的uid_map正好反轉(zhuǎn)。當(dāng)然,你如果在第三個(gè)終端從初始的user namespace里面去看uid_map,是跟之前一樣的。
# 第一個(gè)終端,映射 0 -> 1000
vagrant@ubuntu:~/nstest$ ./userns_child_exec -U -M '0 1000 1' -G '0 1000 1' bash
root@ubuntu:~/nstest# id -u
0
root@ubuntu:~/nstest# id -g
0
root@ubuntu:~/nstest# echo $$
25730
root@ubuntu:~/nstest# cat /proc/$$/uid_map
0 1000 1
root@ubuntu:~/nstest# cat /proc/26091/uid_map
200 0 1
# 第二個(gè)終端,映射 200->1000
vagrant@ubuntu:~/nstest$ ./userns_child_exec -U -M '200 1000 1' -G '200 1000 1' bash
I have no name!@ubuntu:~/nstest$ id -u
200
I have no name!@ubuntu:~/nstest$ echo $$
26091
I have no name!@ubuntu:~/nstest$ cat /proc/$$/uid_map
200 1000 1
I have no name!@ubuntu:~/nstest$ cat /proc/25730/uid_map
0 200 1
# 第三個(gè)終端,初始user namespace里面查看映射關(guān)系
vagrant@ubuntu:~/nstest$ cat /proc/25730/uid_map
0 1000 1
vagrant@ubuntu:~/nstest$ cat /proc/26091/uid_map
200 1000 1
之前我們提到的docker示例中,沒有對user namespace進(jìn)行隔離。user namespace功能雖然在很早就出現(xiàn)了,但是直到Linux kernel 3.8之后這個(gè)功能才逐步穩(wěn)定。docker1.10之后的版本可以通過在docker daemon啟動(dòng)時(shí)加上--userns-remap=[USERNAME]來實(shí)現(xiàn)USER Namespace的隔離,在實(shí)際使用中我們暫時(shí)沒有用到USER namespace的隔離,不過docker對于CAP很早就有使用的,所以可以看到容器啟動(dòng)的時(shí)候如果需要特定功能的需要加--cap-add SYS_ADMIN,NET_ADMIN這些參數(shù)。
8 總結(jié)
docker使用的不是新技術(shù),但是著實(shí)給開發(fā)部署以及應(yīng)用調(diào)度帶來了很大的便利性。特別是docker的overlay網(wǎng)絡(luò)可以實(shí)現(xiàn)容器之間的跨主機(jī)通信,功能很強(qiáng)大。當(dāng)然docker overlay網(wǎng)絡(luò)在大規(guī)模使用的時(shí)候我們項(xiàng)目中也遇到了一些坑,比如在docker1.13.1版本中容器ip在不同主機(jī)復(fù)用的時(shí)候會(huì)導(dǎo)致容器無法連通問題,升級到17.05-ce發(fā)現(xiàn)出現(xiàn)了另外一個(gè)重啟容器后容器之間網(wǎng)絡(luò)連通存在五分鐘延遲的問題,后來升級到17.06.2-ce以后才解決overlay網(wǎng)絡(luò)的BUG。
總體來說,docker現(xiàn)在的版本比較穩(wěn)定,在線上跑過200+容器,除了overlay網(wǎng)絡(luò)那個(gè)問題外,基本沒有出現(xiàn)過大的BUG,值得一試。
參考資料
- https://blog.yadutaf.fr/2014/01/19/introduction-to-linux-namespaces-part-1,2,3,4,5/
- https://lwn.net/Articles/531114/#series_index
- http://pipul.org/2016/02/create-the-container-virtual-network-by-veth-model/
- http://man7.org/linux/man-pages/man7/capabilities.7.html
- http://rk700.github.io/2016/10/26/linux-capabilities/
- http://blog.siphos.be/2013/05/overview-of-linux-capabilities-part-1/
- https://coolshell.cn/articles/17010.html