Docker基礎技術-Linux Namespace

寫這個系列文章主要是對之前做項目用到的docker相關技術做一些總結,包括docker基礎技術Linux命名空間,cgroups,網絡等內容尺上。這是第一篇Linux命名空間,主要參考的introduction-to-linux-namespaces
Namespaces in operation 這兩個系列博客圆到,根據(jù)自己的理解進行了翻譯整合怎抛。示例代碼也全部來自這兩個參考資料,為了學習方便芽淡,我建了個倉庫存放示例代碼以方便測試抽诉,代碼地址見這里,如有錯誤吐绵,歡迎指正迹淌。

1 概述

Linux容器技術(LXC)近幾年十分流行,而其依托的技術并不是很新的東西己单,而是Linux內核自帶的一套內核級別環(huán)境隔離機制唉窃。當然,最流行的LXC技術莫過于docker了纹笼,現(xiàn)在社區(qū)版本更名叫moby了纹份。 Linux容器技術依賴Linux內核的3個主要的隔離機制:chroot,cgroups廷痘,namespace蔓涧。先來看看namespace,在Linux Kernel3.8以后笋额,Linux支持6種namespace元暴。分別是:

namespace 隔離內容 flag
UTS 主機名 CLONE_NEWUTS
IPC 進程間通信 CLONE_NEWIPC
PID chroot進程樹 CLONE_NEWPID
NS(Mount) 掛載點(mount points) CLONE_NEWNS
NET 網絡訪問,包括接口 CLONE_NEWNET
USER 將虛擬的本地UID映射到真實的UID CLONE_NEWUSER

Linux內核提供了一套API用于操作namespace實現(xiàn)環(huán)境隔離兄猩,目前namespace操作的API包括clone(), setns()以及unshare()等茉盏。通過下面的命令我們可以模擬一個類似容器的環(huán)境(隔離了PID namespace,并掛載了proc目錄)枢冤,你可以發(fā)現(xiàn)只能看到bash和shell兩個進程了鸠姨,而且PID是1和2。而在原PID namespace淹真,我們可以看到bash進程的PID則是15011讶迁。個中緣由,且慢慢道來核蘸。

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)建新進程同時創(chuàng)建namespace

代碼 ns.c 從子進程運行 /bin/bash巍糯,先從這個例子來看看Linux namespace的作用(為了簡單起見,略去了錯誤檢查代碼)值纱。

注意到在代碼中使用了clone來代替更常見的fork系統(tǒng)調用鳞贷,clone實際上是Unix系統(tǒng)調用fork的一種更通用的實現(xiàn)方式,它的原型是這樣的

int clone(int (*child_func)(void *), void *child_stack, int flags, void *arg);

child_func參數(shù)為傳遞子進程運行的主函數(shù)虐唠,如ns.c中的child_main搀愧;child_stack參數(shù)為子進程使用的棧空間疆偿,參數(shù)flags可以指定使用的CLONE_*標志咱筛,一次可以指定多個flag;而args則是子進程的參數(shù)杆故。編譯運行上面的代碼迅箩,結果如下所示,運行正常处铛,但是我們很難區(qū)分這是在子進程運行的/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提供了主機名和域名的隔離奕塑,這樣每個容器就有獨立的主機名和域名堂污,從而可以在網絡上被當作一個獨立的節(jié)點而不是宿主機的一個進程。修改clone函數(shù)這行代碼龄砰,加入CLONE_NEWUTS的flag盟猖,然后在子進程中調用sethostname函數(shù),修改后代碼 ns_uts.c换棚。

以root身份運行它

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

可以看到式镐,在子進程中hostname變成了In Namespace,而父進程的hostname為ubuntu不受子進程修改hostname的影響固蚤,通過CLONE_NEWUTS實現(xiàn)了主機名的隔離娘汞。注意,如果不加CLONE_NEWUTS標記運行颇蜡,會發(fā)現(xiàn)退出子進程后hostname也還原了价说,這是因為bash只在登錄的時候讀取一次UTS,等你重新登陸就會發(fā)現(xiàn)hostname變了风秤。因此鳖目,為了hostname隔離,加上CLONE_NEWUTS標志缤弦。

docker容器的hostname也是通過該機制實現(xiàn)的隔離领迈,每個容器都有自己的hostname(默認是容器ID),并不會對宿主機的hostname產生任何影響碍沐。

root@ubuntu:/home/ssj# docker exec -it ssjtestnew /bin/bash
root@c9df3369e321:/# hostname
c9df3369e321

1.2 /proc/PID/ns文件

從/proc/PID/ns目錄中狸捅,我們可以看到一個進程的namespace。比如我們運行上面的 ./ns_uts累提,并查看父子進程的namespace尘喝,結果如下,可以看到ns_uts和子進程bash的ns目錄中斋陪,除了UTS namespace是不一樣的朽褪,表明這兩個進程在不同的UTS名字空間,其他5個namespace是相同的无虚。/proc/PID/ns目錄中的為符號鏈接缔赠,指向的是對應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

當然namespace還有其他的用處,只要namespace的文件描述符是打開的度宦,即便該namespace所有進程都終止了踢匣,該namespace還是依舊存在告匠。我們如果直接退出程序,可以看到進程退出后/proc/PID目錄會整個刪掉离唬,包括ns目錄凫海。于是,為了保存子進程的UTS namespace男娄,我們用mount命令先掛載該namespace,稍后我們會用setns()將進程加入到該UTS namespace漾稀。

mount --bind /proc/3087/ns/uts ~/uts

1.3 加入已經存在的namespace:setns()

通過setns和execve可以讓一個進程加入一個已經存在的namespace并在那個namespace執(zhí)行命令模闲。測試代碼 ns_setns.c,這里用到上一節(jié)中保留的UTS namespace崭捍。

運行結果如下:

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]

可以看到該進程的UTS namespace為我們指定的之前保留的child process的UTS namespace尸折。

1.4 隔離一個namespace:unshare()

unshare函數(shù)可以讓進程脫離一個namespace,它與clone類似殷蛇,不同的是实夹,unshare不需要創(chuàng)建新的進程,而是在當前進程直接隔離namespace粒梦。

測試代碼 ns_unshare.c 亮航,運行之,在參數(shù)中我們傳遞-m用來隔離NS namespace(即掛載點的namespace)匀们,結果可以看到在新的NS namespace的shell進程中umount了一個目錄/run/lock缴淋,并不影響老的shell進程的掛載點。

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操作的相關API函數(shù)已經都說完了重抖,接下來分別看看這6個namespace。

2 UTS Namespace

UTS是實現(xiàn)主機名和域名的隔離祖灰,在第一節(jié)中已經說過钟沛,這里不再贅述。

3 IPC Namespace

IPC指Unix/Linux下進程間通信的方式局扶,可以通過共享內存恨统,信號量,消息隊列详民,管道等方法實現(xiàn)延欠。這里我們要隔離IPC namespace,實現(xiàn)方式也很簡單沈跨,在clone函數(shù)的flags參數(shù)中加入CLONE_NEWIPC即可由捎,這樣你可以在新的namespace中創(chuàng)建IPC,甚至是命名一個饿凛,并不會有與其他應用沖突的風險狞玛。

我們在最初的實例代碼ns.c中修改一下软驰,加入CLONE_NEWIPC的flag。修改的代碼只有一行心肪,如下锭亏。當然這里的CLONE_NEWUTS不是必須的,保留這個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)建一個IPC隊列慧瘤,隊列ID為65536,然后運行./ns_ipc固该,可以看到在新的namespace中并沒有該IPC隊列锅减,做到了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   

接下來可能有人要問了伐坏,那這種父子進程在不同的IPC namespace了怔匣,它們之間怎么通信呢?前面說過桦沉,進程間通信有信號量每瞒,共享內存,管道纯露,F(xiàn)IFO剿骨,sockets等。由于上下文的改變苔埋,使用信號量也許不是最佳方案懦砂。而使用共享內存則有效率上的問題,如果不隔離網絡棧的話也可以用sockets组橄,但是我們現(xiàn)在要一步步隔離一切荞膘,因此sockets也不合適。FIFO則可以用于任意進程間的通信玉工,F(xiàn)IFO是一種特殊的文件類型羽资,在文件系統(tǒng)中是有對應路徑的,它的問題也與sockets類似遵班,因為我們要隔離文件系統(tǒng)的話屠升,它也不合適。管道用于有親屬關系的進程之間通信狭郑,比如父子進程或者兄弟進程之間通信腹暖,很適合不同namespace的進程通信。

使用管道實現(xiàn)不同namespace之間進程通信的示例代碼 ns_ipc.c翰萨,運行之脏答,可以看到位于不同namespace的父子進程確實通信成功了。

root@ubuntu:/home/vagrant/nstest# ./ns_ipc
 - Hello ?
 - World !
root@In Namespace:/home/vagrant/nstest# exit
root@ubuntu:/home/vagrant/nstest# 

4 PID Namespace

實現(xiàn)PID隔離加上CLONE_NEWPID標識即可绢涡。示例代碼 ns_pid.c 山害,運行之:

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中運行的/bin/bash的PID為1。而它的父進程的PID是7627柬甥。而在父進程namespace中黄绩,可以看到/bin/bash的進程為7268羡洁。如果你試圖在新的Namespace中去kill某個不同namespace中的進程,則會報錯提示進程不存在爽丹,達到了進程隔離的目的筑煮。

要注意的是,這個時候你在新的namespace中用ps粤蝎,top等命令去查看咆瘟,會發(fā)現(xiàn)7627這個進程是可見的。這與我們在docker容器中看到的不一致诽里,如在我創(chuàng)建的一個redis容器中,用ps, top其實是只看得到容器所在namespace的進程的飞蛹。

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

這是因為ps命令讀取的是/proc文件系統(tǒng)獲取的信息谤狡,而文件系統(tǒng)我們還沒有隔離,所以在新的namespace中可以看到所有的進程卧檐,接下來我們會用NS namespace來實現(xiàn)這個隔離墓懂。

5 NS Namespace

NS namespace也就是掛載點相關的了,在第4節(jié)的代碼基礎上加入CLONE_NEWNS的flag霉囚,并在子進程掛載 /proc目錄捕仔。修改后創(chuàng)建進程的代碼 ns_ns.c, 運行之:

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命令確實只顯示了當前namespace下面的進程了,而且ls /proc/命令查看發(fā)現(xiàn)/proc目錄下面的內容也清爽多了盈罐。docker使用NS namespace實現(xiàn)了一些文件系統(tǒng)的掛載榜跌,原理與這個類似,結合chdir和chroot可以實現(xiàn)一個山寨的docker鏡像盅粪。

這個時候我們再來看看docker中PID和NS namespace具體的實現(xiàn)(我的docker版本是1.13.1钓葫,其他版本可能有所不同),我這里在宿主機起了一個redis容器名為redistest票顾,通過pstree可以看到進程關系如下:

        |-dockerd-+-docker-containerd-+-docker-containerd-shim-+-redis-server---3*[{redis-server}]
        |         |                 |                 `-8*[{docker-containe}]
        |         |                 `-12*[{docker-containe}]
        |         `-19*[{dockerd}]

這里對應進程關系就是:

  • dockerd進程創(chuàng)建了一個docker-containerd子進程础浮,而docker-contianerd子進程再創(chuàng)建子進程docker-containerd-shim,也就是對應具體容器的進程奠骄。
  • 容器進程docker-containerd-shim創(chuàng)建容器里面的1號進程redis-server豆同。
  • 通過查看/proc/PID/ns目錄就可以發(fā)現(xiàn),dockerd含鳞,dockerd-containerd以及dockerd-containerd-shim的namespace都是一樣的影锈,而容器里面的1號進程 redis-server的namespace除了User namespace外,其他的namespace都已經不同。也就是說精居,從容器里面的1號進程開始锄禽,進程的namespace開始隔離。
  • 另外注意一點的是靴姿,當你使用 docker exec -it redistest /bin/bash命令進入容器的時候沃但,這個/bin/bash進程的父進程其實是另外一個 docker-containerd-shim進程,只是/bin/bash進程的namespace和redis-server進程一樣佛吓,所以這個時候你在redistest容器中ps -ef宵晚,可以看到除了redis-server進程外,還有/bin/bash進程维雇。通過exec命令進入容器后淤刃,再來看進程關系,是下面這樣的:
       |-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是指網絡上的隔離逸贾,通過加入CLONE_NEWNET來實現(xiàn)。在討論這個之前津滞,可以先看看通過ip命令如何手動創(chuàng)建network namespace以及veth設備等铝侵。veth主要的目的是為了跨NET namespace之間提供一種類似于Linux進程間通信的技術,所以veth總是成對出現(xiàn)触徐,如下面的veth0和veth1咪鲜。它們位于不同的NET namespace中,在veth設備任意一端接收到的數(shù)據(jù)撞鹉,都會從另一端發(fā)送出去疟丙。veth工作在L2數(shù)據(jù)鏈路層,只負責數(shù)據(jù)傳輸鸟雏,不會更改數(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í)行完成后,我們可以在宿主機里面看到網絡設備是這樣的:

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這個NET namespace中孝鹊,看到的網絡設備是這樣的:

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 

這個原理就是先創(chuàng)建一個新的NET namespace名為demo拂蝎,然后創(chuàng)建一對veth設備,veth0和veth1惶室,接著將veth1移動到namespace demo温自,而veth0仍然保留在原來的namespace,然后啟動對應的veth設備皇钞。這樣一對veth設備分屬于不同的namespace悼泌,并可以通信。然后給veth0和veth1設置ip并啟動它們夹界。要查看veth的一對設備中另外一個馆里,可以用 ethtool -S命令。實現(xiàn)上面功能的代碼 ns_net.c ,運行之鸠踪,如下:

root@ubuntu:/home/vagrant/nstest# ./ns_net 
 - [ 2760] Hello ?
 - [    1] World !

### 宿主機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網絡分為bridge丙者, host, overlay等幾種類型营密。host就是與主機共用namespace械媒,這里不單獨分析了。而bridge就與前面例子中類似评汰,不同的是僅僅有veth容器還無法與外部聯(lián)通纷捞,因此docker借助了網橋技術用于連接不同網段,在L2層進行數(shù)據(jù)轉發(fā)被去,將veth0加入到宿主機的網橋docker0中主儡,并在iptables加入對應的NAT規(guī)則,以保證容器可以與外部連通惨缆。注意docker中NET namespace的隔離不是通過ip命令實現(xiàn)的(因為不是所有的內核版本都有ip netns這個高級命令)糜值,而是通過netlink基于操作系統(tǒng)調用的方式實現(xiàn)的。而overlay網絡則是通過vxlan協(xié)議實現(xiàn)坯墨,對應的veth會橋接到overlay的NET namespace一個br0網橋上臀玄。bridge和overlay網絡的一個示意圖如下(圖來自 deep-dive-into-docker-overlay-networks),其中192.168.0.X這個是自定義的overlay網絡畅蹂,而172.18.0.X的則是bridge網絡,docker網絡部分會在下一篇文章再詳細分析荣恐。

docker網絡示意圖

7 USER Namespace

7.1 創(chuàng)建新的USER Namespace

加上 CLONE_NEWUSER flag可以實現(xiàn)USER namespace的隔離液斜。示例如下(注意,在debian或者ubuntu中必須設置/proc/sys/kernel/unprivileged_userns_clone這個文件值為1叠穆,否則無法以普通用戶運行帶CLONE_NEWUSER標記的clone命令
)
示例代碼 ns_user.c少漆,以普通用戶運行之:

vagrant@ubuntu:~/nstest$ id -u
1000
vagrant@ubuntu:~/nstest$ id -g
1000
vagrant@ubuntu:~/nstest$ gcc -o ns_user ns_user.c -lcap  
#如果編譯報錯的話,安裝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

這里有幾點注意的:

  • 其一示损,從capabilities輸出可以看到子進程在它的namespace里面有全部的capability,雖然我們是用普通用戶權限運行的程序嚷硫。當一個新的USER namespace創(chuàng)建的時候检访,這個namespace的第一個進程就被賦予了全部的capability。capability是為了實現(xiàn)更精細化的權限控制而加入的仔掸。(我們以前熟知通過設置文件的SUID位脆贵,這樣非root用戶的可執(zhí)行文件運行后的euid會成為文件的擁有者ID,比如passwd命令運行起來后有root權限起暮。一旦SUID的文件存在漏洞卖氨,便可能被利用而增加安全風險)。查看文件的capability的命令為 filecap -a,而查看進程capability的命令為 pscap -a(pscap和filecap工具需要安裝 libcap-ng-utils這個包)筒捺。對capability的那串數(shù)字解碼命令為 capsh --decode=00000000000000c0柏腻。更多capability的內容見參考資料4。

    對于capability系吭,可以看一個簡單的例子便于理解五嫂。如ubuntu14.04系統(tǒng)中自帶的ping工具,它是有設置SUID位的村斟。這里拷貝ping到我的用戶目錄下名為anotherping贫导,可以看到它的SUID位是沒有了的,運行anotherping蟆盹,會提示權限錯誤孩灯。這里,我們只要將其加上 cap_net_raw權限即可逾滥,不需要設置SUID位那么大的權限峰档。

    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
    ...
    
  • 其二,一個進程的uid和gid在不同的USER namespace是可以不一樣的寨昙,這需要一個namespace內部映射到namespace外部的映射關系讥巡。這樣當一個USER namespace中的進程的操作可能影響到外部系統(tǒng)時,可以對這個進程的權限進行檢查舔哪。如果一個用戶ID在USER namespace中沒有映射關系欢顷,則getuid()系統(tǒng)調用會返回 /proc/sys/kernel/overflowuid值作為用戶ID,這個值默認為65534捉蚤,就如我們前面程序中輸出一樣(gid對應的文件名為overflowgid)抬驴。

  • 其三,盡管通過clone系統(tǒng)調用創(chuàng)建的子進程在新的USER namespace中有所有權限缆巧,但是它在parent user namespace是沒有任何權限的布持,即便以root身份運行也是一樣。user namespace的創(chuàng)建可以是嵌套的陕悬,一個user namespace一定有個parent user namespace题暖,可以有零或者多個 child user namespace。子進程的parent user namespace就是調用clone()或者unshare()通過CLONE_NEWUSER的flag創(chuàng)建新namespace的那個父進程的user namespace捉超。

7.2 映射uid和gid

創(chuàng)建新的user namespace之后第一步就是設置好user和group的映射關系胧卤。這個映射通過設置/proc/PID/uid_map(gid_map)實現(xiàn),格式如下:

    ID-inside-ns   ID-outside-ns   length

不是所有的進程都能隨便修改映射文件的拼岳,必須同時具備如下條件:

  • 修改映射文件的進程必須有PID進程所在user namespace的CAP_SETUID/CAP_SETGID權限灌侣。進程的capability一般是通過其可執(zhí)行文件的capability獲得。
  • 修改映射文件的進程必須是跟PID在同一個user namespace或者PID的parent user namespace裂问。
  • 映射文件uid_mapgid_map只能寫入一次侧啼,再次寫入會報錯牛柒。

下面來測試下7.1中的例子:

#在第一個終端運行 ns_user
vagrant@ubuntu:~/nstest$ ./ns_user x
eUID = 65534;  eGID = 65534; capabilities: = ...ep

#在第二個終端寫入該進程對應的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

#第一個終端此時輸出為:
vagrant@ubuntu:~/nstest$ ./ns_user x
eUID = 0;  eGID = 65534; capabilities: = ...ep

#在第二個終端繼續(xù)寫入gid_map
vagrant@ubuntu:~/nstest$ echo '0 1000 1' > /proc/8776/gid_map

#第一個終端此時輸出為:
vagrant@ubuntu:~/nstest$ ./ns_user x
eUID = 0;  eGID = 0; capabilities: = ...ep

可以看到,我們在位于parent user namespace的bash進程中通過echo命令修改uid_mapgid_map都是可以成功的痊乾。這是因為我的測試環(huán)境的bash進程具有CAP_SETUIDCAP_SETGID權限的皮壁,查看/proc/PID/status可以驗證進程的權限或者getcap可以驗證一個可執(zhí)行文件的權限,如下驗證bash的權限哪审,如果bash原來沒有這兩個權限蛾魄,可以通過命令sudo setcap cap_setgid,cap_setuid+ep /bin/bash設置:

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

這里有個要注意的地方,ubuntu14.04的/bin/bash文件默認就有修改新的user namespace進程的uid_map的權限湿滓,如果要修改gid_map要另外加下cap_setgid權限滴须。而其他的可執(zhí)行文件,默認也是只有cap_setuid權限叽奥,比如網上很多文章中提到的一個設置user namespace的例子扔水,在ubuntu14.04里面設置gid_map會失敗,因為可執(zhí)行文件沒有cap_setgid權限朝氓,需要加上gid權限才能成功修改gid_map魔市。

看這個例子,代碼 ns_child_exec.c赵哲,執(zhí)行后可以發(fā)現(xiàn)在新的user namespace里面的bash里面通過echo命令設置uid_map和gid_map都會失敗待德,這是因為當一個非root用戶的進程執(zhí)行execve()時,進程的capability會被清空枫夺。于是将宪,子進程雖然有新的user namespace所有的權限集合,但是通過它exevce執(zhí)行的bash進程以及bash進程的子進程是沒有對應的capability的橡庞。

vagrant@ubuntu:~/nstest$ ./ns_child_exec -U bash
nobody@ubuntu:~/nstest$ id -u  #新的user namespace運行的bash進程
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

為了設置映射文件较坛,因此需要在父進程中設置,示例代碼 userns_child_exec.c毙死。注意一點的是,要在userns_child_exec進程中成功設置gid_map文件喻鳄,需要給可執(zhí)行文件加上 cap_setgid權限扼倘,此外,還要保證 /bin/bash是有cap_setgid權限的:

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

最后一點要注意的是除呵,uid_map文件里面的 ID-outside-ns 這個值是根據(jù)當前讀取文件的user namespace生成的再菊,這個是什么意思呢?看下面的例子就明白了颜曾。在兩個終端里面分別運行 userns_child_exec程序纠拔,設置不同的ID-inside-ns,運行結果如下所示泛豪。也就是說稠诲,我們在初始的user namespace創(chuàng)建了2個child user namespace侦鹏,一個是映射的uid為0,另一個映射的為200臀叙,在第一個終端看第二個終端進程對應的映射關系時可以發(fā)現(xiàn)uid_map值為 200 0 1略水,也就是說第二個user namespace中的進程用戶ID映射到了當前user namespace的uid 0,而不是初始的user namespace的1000劝萤。從第二個終端里面看第一個終端的進程的uid_map正好反轉渊涝。當然,你如果在第三個終端從初始的user namespace里面去看uid_map床嫌,是跟之前一樣的跨释。

# 第一個終端,映射 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

# 第二個終端厌处,映射 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

# 第三個終端鳖谈,初始user namespace里面查看映射關系
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進行隔離嘱蛋。user namespace功能雖然在很早就出現(xiàn)了蚯姆,但是直到Linux kernel 3.8之后這個功能才逐步穩(wěn)定。docker1.10之后的版本可以通過在docker daemon啟動時加上--userns-remap=[USERNAME]來實現(xiàn)USER Namespace的隔離洒敏,在實際使用中我們暫時沒有用到USER namespace的隔離龄恋,不過docker對于CAP很早就有使用的,所以可以看到容器啟動的時候如果需要特定功能的需要加--cap-add SYS_ADMIN凶伙,NET_ADMIN這些參數(shù)郭毕。

8 總結

docker使用的不是新技術,但是著實給開發(fā)部署以及應用調度帶來了很大的便利性函荣。特別是docker的overlay網絡可以實現(xiàn)容器之間的跨主機通信显押,功能很強大。當然docker overlay網絡在大規(guī)模使用的時候我們項目中也遇到了一些坑傻挂,比如在docker1.13.1版本中容器ip在不同主機復用的時候會導致容器無法連通問題乘碑,升級到17.05-ce發(fā)現(xiàn)出現(xiàn)了另外一個重啟容器后容器之間網絡連通存在五分鐘延遲的問題,后來升級到17.06.2-ce以后才解決overlay網絡的BUG金拒。

總體來說兽肤,docker現(xiàn)在的版本比較穩(wěn)定,在線上跑過200+容器绪抛,除了overlay網絡那個問題外资铡,基本沒有出現(xiàn)過大的BUG,值得一試幢码。

參考資料

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末笤休,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子症副,更是在濱河造成了極大的恐慌店雅,老刑警劉巖政基,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異底洗,居然都是意外死亡腋么,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門亥揖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來珊擂,“玉大人,你說我怎么就攤上這事费变〈萆龋” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵挚歧,是天一觀的道長扛稽。 經常有香客問我,道長滑负,這世上最難降的妖魔是什么在张? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮矮慕,結果婚禮上帮匾,老公的妹妹穿的比我還像新娘。我一直安慰自己痴鳄,他們只是感情好瘟斜,可當我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著痪寻,像睡著了一般螺句。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上橡类,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天蛇尚,我揣著相機與錄音,去河邊找鬼顾画。 笑死取劫,一個胖子當著我的面吹牛,可吹牛的內容都是我干的亲雪。 我是一名探鬼主播勇凭,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼疚膊,長吁一口氣:“原來是場噩夢啊……” “哼义辕!你這毒婦竟也來了?” 一聲冷哼從身側響起寓盗,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤灌砖,失蹤者是張志新(化名)和其女友劉穎璧函,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體基显,經...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡蘸吓,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了撩幽。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片库继。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖窜醉,靈堂內的尸體忽然破棺而出宪萄,到底是詐尸還是另有隱情,我是刑警寧澤榨惰,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布拜英,位于F島的核電站,受9級特大地震影響琅催,放射性物質發(fā)生泄漏居凶。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一藤抡、第九天 我趴在偏房一處隱蔽的房頂上張望侠碧。 院中可真熱鬧,春花似錦杰捂、人聲如沸舆床。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽挨队。三九已至,卻和暖如春蒿往,著一層夾襖步出監(jiān)牢的瞬間盛垦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工瓤漏, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留腾夯,地道東北人。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓蔬充,卻偏偏與公主長得像蝶俱,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子饥漫,可洞房花燭夜當晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內容

  • 轉載自 http://blog.opskumu.com/docker.html 一榨呆、Docker 簡介 Docke...
    極客圈閱讀 10,501評論 0 120
  • 一、Docker 簡介 Docker 兩個主要部件:Docker: 開源的容器虛擬化平臺Docker Hub: 用...
    R_X閱讀 4,388評論 0 27
  • 五庸队、Docker 端口映射 無論如何积蜻,這些 ip 是基于本地系統(tǒng)的并且容器的端口非本地主機是訪問不到的闯割。此外,除了...
    R_X閱讀 1,751評論 0 7
  • 起車 1.教練好 2.調座椅,右手撥弄座椅下方把手丙笋,調距離谢澈,左腳蹬車踏板,背靠座椅御板,細聽聲音(一拳零一指)試腳感 ...
    瑞曦2017閱讀 264評論 0 0