我保证这篇文章会给你一些不一样的东西,I promise.
Docker大红大紫之时,我错过了什么,可能是因为我并没有必须使用Docker的动机,毕竟我不是编程者,我也不需要发布什么配置复杂的系统,我是一个典型的实用主义者,也可以理解为消费主义,我从来不学习那些当前自己并不需要的东西。
归到缺点,你可以说我不懂得未雨绸缪,但是反过来,也可以说我随机应变见招拆招,不管怎么说吧,个人风格,自己怎么看都是对的。
但是近期用到Docker了,总要记录以备忘,就趁着周末的雨夜写下本文。OK,接下来的时间属于Docker。
本文并不会详细描述Docker的用法,本文的目的是解析Docker得以成型所依托的核心组件原理,以Linux平台为例,我们看看是什么内核特性成就了Docker这么一个伟大的东西。
我总结为四件套,分别是Linux Namespace,Linux Cgroup,Linux OverlayFS,Linux虚拟网卡。
新的视角-firejail
和其它文章不同,我不准备一开始就把Docker分解为这些组件,而是采用另外一条相反的路线,通过另外一个东西,即firejail来解析这四件套,最后我们看看它们是如何合并成一个和Docker很类似的容器的。
之所以用firejail来描述,是因为它足够简单,没有那些外围的东西,比如守护进程,C/S模型,配置文件,DSL等等。它在Linux发行版上就是简单的一条命令,而我的目的正是通过这么一条命令,构建一个和Docker类似的东西。
那么说来说去,什么是firejail?除了我给出的超链接之外,看manual的DESCRIPTION:
Firejail is a SUID sandbox program that reduces the risk of security breaches by restricting the running environment of untrusted applications using Linux
.
namespaces, seccomp-bpf and Linux capabilities. It allows a process and all its descendants to have their own private view of the globally shared kernel
resources, such as the network stack, process table, mount table. Firejail can work in a SELinux or AppArmor environment, and it is integrated with Linux
Control Groups.
.
Written in C with virtually no dependencies, the software runs on any Linux computer with a 3.x kernel version or newer. It can sandbox any type of pro-
cesses: servers, graphical applications, and even user login sessions.
.
Firejail allows the user to manage application security using security profiles. Each profile defines a set of permissions for a specific application or
group of applications. The software includes security profiles for a number of more common Linux programs, such as Mozilla Firefox, Chromium, VLC, Transmis-
sion etc.
firejail简单到什么程度呢?你只需要在命令行敲入firejail,然后用某种debug hacker的精神去探究,你就能发现一切秘密。现在开始:
root@debian:/home/zhaoya# firejail # 启动firejailReading profile /etc/firejail/server.profileReading profile /etc/firejail/disable-common.incReading profile /etc/firejail/disable-programs.incReading profile /etc/firejail/disable-passwdmgr.inc** Note: you can use --noprofile to disable server.profile **Parent pid 13389, child pid 13390The new log directory is /proc/13390/root/var/logChild process initializedroot@debian:~# ps -e PID TTY TIME CMD 1 ? 00:00:00 firejail #神奇的事情,firejail成了1号进程 3 ? 00:00:00 bash 8 ? 00:00:00 psroot@debian:~# # OK,我们已经到了新的PID Namespace!欢迎到来!
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
话说,我太喜欢这种简化纪要的风格了,还记得我用超级简单的simpletun来解释超级凌乱的OpenVPN吗?这一次,舞台没变,换了演员而已。
除了依然采用这种用原理相同的简单小toy去解释复杂的大家伙方式之外,没有源码分析依然是我秉承的一贯风格。如果你真的理解了原理,你就一定能用代码以外的方式去表达这个原理,反之,看懂源码则只是你梳理好了一种实现方式的逻辑,事实上,实现方式远远不止这一种,这就是为什么很多人看懂了源码就说自己精通,最后实际上就是连What & How都不知道,就聊Why。嗯,只有在Debug的时候才会deep into源码…
现在开始。
Linux Namespace
首先,什么是Namespace?
任何基础设施都需要以某种方式被管理,系统往往需要对被管理组件进行编址,编址往往是全局唯一的,这里所谓的全局指的就是在一个Namespace(命名空间)内的全局。引用Wiki上的描述:
命名空间(英语:Namespace,日語:名前空間),也称名字空间、名称空间等,它表示着一个标识符(identifier)的可见范围。 一个标识符可在多个命名空间中定义,它在不同命名空间中的含义是互不相干的。
Linux内核目前支持以下几种Namespace:
Namespace | 解释 |
---|---|
PID Namespace | 每一个Namespace中的进程PID是独立编址的,不同Namespace中的进程编址空间彼此重合。值得注意的是,PID Namespace是一个层级结构,Child对Parent可见,反之不可见。 |
Net Namespace | Net Namespace隔离了整个网络协议栈,包括路由表,网卡IP地址,端口号,Netfilter/iptables等在每一个Net Namespace中均是独立的。 |
Mount Namespace | 每一个Mount Namespace均有一个独立的文件系统层级视图。 |
UTS Namespace | 与本文内容关系不大,不解释 |
IPC Namespace | 与本文内容关系不大,不解释 |
User Namespace | 与本文内容关系不大,不解释 |
接下来就分别解释一下这些个Namespace。
PID Namespace
我们知道,Linux进程管理是基于进程pid的,所有的进程均被分配一个PID Namespace内唯一的PID作为其标识。除此之外,关于PID的管理非常复杂,这涉及到UNIX/Linux的进程模型,详情可以参考下面的文章:
朴素的UNIX之-进程/线程模型:https://blog.csdn.net/dog250/article/details/40208219
这个文章里有一幅大图,详细解释了PID,TGID等ID的关系,本文为了简单起见,就假设只有进程这么一个PID,不再考虑线程和进程组。
我们来看上一个小节最后的那个实验,敲入firejail命令后,系统进入了一个新的PID Namespace,其中有自己的1号进程,此时如果我们另起一个终端,在该PID Namespace外部看,看看有没有什么发现,我们用pstree命令看一下层级关系:
root@debian:/home/zhaoya/overlayjail# pstree -p... | |-sshd(44780)---sshd(44786)-+-bash(13372)---su(13384)---bash(13385)---firejail(13389)---firejail(13390)---bash(13393) # 注意此处! | | `-bash(29824)---su(29836)---bash(29841)---pstree(15273)
- 1
- 2
- 3
- 4
我们发现自firejail衍生出来的一个bash,其PID实13393,然而在其独立的PID Namespace中,它的PID则是3,很容易确认它们是同一个进程:
root@debian:~# ls -l /proc/3/ns/pid # 独立PID Namespace中执行lrwxrwxrwx 1 root root 0 Jul 12 19:56 /proc/3/ns/pid -> pid:[4026532135]root@debian:~# ... # 切换一个终端root@debian:/home/zhaoya/overlayjail# ls -l /proc/13393/ns/pid # 外部执行 lrwxrwxrwx 1 root root 0 Jul 12 19:52 /proc/13393/ns/pid -> pid:[4026532135]root@debian:/home/zhaoya/overlayjail#
- 1
- 2
- 3
- 4
- 5
- 6
- 7
我们发现它们的PID Namespace确实就是同一个“pid:[4026532135]”。这说明了PID Namespace的一个层级结构:
在内核代码中,这个层级关系体现在alloc_pid函数(不得已,这段代码非常简单地解释了为什么父PID Namespace能看到子PID Namespace的进程,比用语言描述要简单很多。):
pid->level = ns->level; # 只要在clone调用中有NEWPID标识,新的NS level便会在当前NS level的基础上加1,以记录层级关系。for (i = ns->level; i >= 0; i--) { nr = alloc_pidmap(tmp); if (nr < 0) goto out_free; pid->numbers[i].nr = nr; pid->numbers[i].ns = tmp; tmp = tmp->parent;}// 一个for循环过后,一个task便拥有了从本PID NS一直到最上层PID NS的所有NS中的唯一PID。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
和进程的父子关系类似,PID Namespace也维持这么一个关系,即子PID Namespace对父PID Namespace是可见的,每一个子PID Namespace在其所有上层的PID Namespace中均有唯一的PID编址。这一点是个其它别的Namespace不同的地方,值得注意。
父PID Namespace是可以操作子PID Namespace里面的进程的,当我们在外部父PID Namespace中renice子PID Namespace中的bash后,后者的优先级随即发生了变化:
首先看firejail中的进程优先级:
root@debian:~# ps -elfF S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD5 S root 1 0 0 80 0 - 4566 - 19:16 ? 00:00:00 firejail0 S root 3 1 0 80 0 - 5285 - 19:16 ? 00:00:00 /bin/bash0 R root 20 3 0 80 0 - 9576 - 20:04 ? 00:00:00 ps -elfroot@debian:~#
- 1
- 2
- 3
- 4
- 5
- 6
随机用外部父PID Namespace中的PID renice其bash的优先级:
root@debian:/home/zhaoya/overlayjail# renice -n 10 1339313393 (process ID) old priority 0, new priority 10
- 1
- 2
然后再firejail里面看:
root@debian:~# ps -elfF S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD5 S root 1 0 0 80 0 - 4566 - 19:16 ? 00:00:00 firejail0 S root 3 1 0 90 10 - 5285 - 19:16 ? 00:00:00 /bin/bash # 已然改变!0 R root 21 3 0 90 10 - 9576 - 20:06 ? 00:00:00 ps -elfroot@debian:~# root@debian:~#
- 1
- 2
- 3
- 4
- 5
- 6
- 7
你甚至可以kill掉进程,发信号等等。
因此,我们得知,PID Namespace在纵向上依然保留着管理关系,并没有完全隔离,在横向上则是完全隔离的,比如非直系继承的兄弟叔伯之间便无法使能这种管理动作。
以上便是PID Namespace的理论机制,我们已经知道firejail已经在新的PID Namespace中clone出了一个bash,那么此后在此bash中执行的所有的命令以及启动的所有的daemon均属于这个PID Namespace了,只要父PID Namespace不干预,它们和其它的PID Namespace中的进程就是隔离的了,互相不可见。这完成了容器隔离的第一步,Docker的实现在这一步与firejail完全一致。
在进入Net Namespace的分析之前,我们想一下父PID Namespace什么情况下会干预子PID Namespace中的进程呢?最为直观的答案似乎是,即在子PID Namespace占用了大量资源的时候,这个时候就不得不行使家长制权力了,为了避免这一点,Linux内核拥有新的机制来限制子PID Namespace的资源,即Cgroup机制,这个下文会说。
接下来看看Net Namespace。我们exit退出firejail,然后重新启动,这次携带一个参