以firejail sandbox解析Docker核心原理依赖的四件套

栏目: 服务器 · Linux · 发布时间: 8年前

内容简介:我保证这篇文章会给你一些不一样的东西,I promise.Docker大红大紫之时,我错过了什么,可能是因为我并没有必须使用Docker的动机,毕竟我不是编程者,我也不需要发布什么配置复杂的系统,我是一个典型的实用主义者,也可以理解为消费主义,我从来不学习那些当前自己并不需要的东西。归到缺点,你可以说我不懂得未雨绸缪,但是反过来,也可以说我随机应变见招拆招,不管怎么说吧,个人风格,自己怎么看都是对的。

我保证这篇文章会给你一些不一样的东西,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  # 启动firejail
Reading profile /etc/firejail/server.profile
Reading profile /etc/firejail/disable-common.inc
Reading profile /etc/firejail/disable-programs.inc
Reading profile /etc/firejail/disable-passwdmgr.inc

** Note: you can use --noprofile to disable server.profile **

Parent pid 13389, child pid 13390
The new log directory is /proc/13390/root/var/log
Child process initialized
root@debian:~# ps -e
   PID TTY          TIME CMD
     1 ?        00:00:00 firejail #神奇的事情,firejail成了1号进程
     3 ?        00:00:00 bash
     8 ?        00:00:00 ps
root@debian:~# # OK,我们已经到了新的PID Namespace!欢迎到来!

话说,我太喜欢这种简化纪要的风格了,还记得我用超级简单的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)

我们发现自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# 

我们发现它们的PID Namespace确实就是同一个“pid:[4026532135]”。这说明了PID Namespace的一个层级结构:

以firejail sandbox解析Docker核心原理依赖的四件套

在内核代码中,这个层级关系体现在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。

和进程的父子关系类似,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 -elf
F S UID         PID   PPID  C PRI  NI ADDR SZ WCHAN  STIME TTY          TIME CMD
5 S root          1      0  0  80   0 -  4566 -      19:16 ?        00:00:00 firejail
0 S root          3      1  0  80   0 -  5285 -      19:16 ?        00:00:00 /bin/bash
0 R root         20      3  0  80   0 -  9576 -      20:04 ?        00:00:00 ps -elf
root@debian:~#

随机用外部父PID Namespace中的PID renice其bash的优先级:

root@debian:/home/zhaoya/overlayjail# renice -n 10 13393
13393 (process ID) old priority 0, new priority 10

然后再firejail里面看:

root@debian:~# ps -elf
F S UID         PID   PPID  C PRI  NI ADDR SZ WCHAN  STIME TTY          TIME CMD
5 S root          1      0  0  80   0 -  4566 -      19:16 ?        00:00:00 firejail
0 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 -elf
root@debian:~# 
root@debian:~#

你甚至可以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,然后重新启动,这次携带一个参数:

root@debian:/home/zhaoya# firejail --net=enp0s17
Reading profile /etc/firejail/server.profile
Reading profile /etc/firejail/disable-common.inc
Reading profile /etc/firejail/disable-programs.inc
Reading profile /etc/firejail/disable-passwdmgr.inc

** Note: you can use --noprofile to disable server.profile **

Parent pid 17137, child pid 17138
The new log directory is /proc/17138/root/var/log

Interface        MAC                IP               Mask             Status
lo                                  127.0.0.1        255.0.0.0        UP    
eth0-17137       ca:7a:9a:b2:7d:db  192.168.44.253   255.255.255.0    UP    
Default gateway 192.168.44.2

Child process initialized
root@debian:~# ps -e
   PID TTY          TIME CMD
     1 ?        00:00:00 firejail
     3 ?        00:00:00 bash
     8 ?        00:00:00 ps
root@debian:~# 

嗯,似乎发生了一点变化。

Net Namespace

紧接着上节留下的悬念,我们check一下这个新的PID Namespace在哪个Net Namespace中:

root@debian:~# ls -l /proc/1/ns/net  # firejail中执行
lrwxrwxrwx 1 root root 0 Jul 12 20:30 /proc/1/ns/net -> net:[4026532141]
root@debian:~# 
... # 切换一个终端
root@debian:/home/zhaoya/overlayjail# ls -l /proc/1/ns/net  # 外部执行
lrwxrwxrwx 1 root root 0 Jul  9 20:04 /proc/1/ns/net -> net:[4026531957]
root@debian:/home/zhaoya/overlayjail# 

显然 “net:[4026531957] != net:[4026532141]” ,嗯,它们没有在一个Net Namespace!

firejail的–net参数,含义就是另外新建一个Net Namespace。我们看看这意味着什么。

如果没有–net参数,那么即便是firejail新创建了一个PID Namespace,那么两个firejail也仅仅是在PID进程管理层面上实现了隔离,两个firejail里面的进程依然看到的是同一块网卡上同一个IP地址,同一张系统的路由表,同一份iptables规则…如果新创建一个Net Namespace,意味着这些都可以隔离了,意思是两个不同的firejail中的进程将看到两套不同的网络协议栈设施。

基本上Net Namespace就这些内容,和PID Namespace相比,它显得非常简单,就像下图一样:

以firejail sandbox解析Docker核心原理依赖的四件套

它没有层级结构,没有父子关系,完全就是一个平坦隔离的模型,非常简单。值得注意的是, 每一张网卡,不管是物理网卡还是虚拟网卡,均只能同时在一个Net Namespace中 ,也就是说,如果网卡A被Net Namespace1用了,那么Net Namespace2便无法看到这张网卡,这个问题促使了各种网卡虚拟化技术的发展,其中的veth这种简单的以及pf/vf这种复杂的自不必说,比较好玩的就是MACVLAN,IPVLAN等技术,即在不需要任何硬件机制支持的基础上将一张物理网卡虚拟成多张不同的虚拟网卡,这个下文会简述。

…等等其它Namespace

在PID和Net层面均实现了Namespace隔离之外,我们的firejail欲至何方?它离Docker还有多远?

本来我是想继续说Mount Namespace的,但是和上面的两个Namespace相比较,也是大同小异,所谓的Mount Namespace无非就是实现了隔离的文件系统视图,在更加简单的的层面上,我想用几乎所有人都懂的chroot来模拟这个,虽然不严谨,但是意思传达了。在下面的 “合并为Docker” 这一小节,我将用chroot来完成最终的合并。

顺便说一句,Docker确实使用了Mount Namespace,我们可以运行一个Docker容器以确认:

root@debian:/home/zhaoya# docker run -it --name checkNS --hostname zhaoya debian bash
root@zhaoya:/# 
root@zhaoya:/# ls -l /proc/1/ns/*
lrwxrwxrwx 1 root root 0 Jul 13 04:39 /proc/1/ns/cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 root root 0 Jul 13 04:39 /proc/1/ns/ipc -> ipc:[4026532140]
lrwxrwxrwx 1 root root 0 Jul 13 04:39 /proc/1/ns/mnt -> mnt:[4026532134]
lrwxrwxrwx 1 root root 0 Jul 13 04:39 /proc/1/ns/net -> net:[4026532143]
lrwxrwxrwx 1 root root 0 Jul 13 04:39 /proc/1/ns/pid -> pid:[4026532141]
lrwxrwxrwx 1 root root 0 Jul 13 04:39 /proc/1/ns/user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Jul 13 04:39 /proc/1/ns/uts -> uts:[4026532135]
root@zhaoya:/# 

我们再看看容器外部的宿主机的1号进程对应的Namespace:

root@debian:/home/zhaoya# ls -l /proc/1/ns/*   
lrwxrwxrwx 1 root root 0 Jul  9 20:04 /proc/1/ns/cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 root root 0 Jul  9 20:04 /proc/1/ns/ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 Jul  9 20:04 /proc/1/ns/mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0 Jul  9 20:04 /proc/1/ns/net -> net:[4026531957]
lrwxrwxrwx 1 root root 0 Jul  9 20:04 /proc/1/ns/pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 Jul  9 20:04 /proc/1/ns/user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Jul  9 20:04 /proc/1/ns/uts -> uts:[4026531838]
root@debian:/home/zhaoya# 

在接下来的内容中,我绕过新建Mount Namespace这个过程,采用简化的chroot来描述firejail的对应过程,请注意不同点。

此外,其它的几个Namespace,与本文无关,便不说了。

Linux Cgroup

如果说Linux Namespace是从编址的角度对被管理实体进行了有效隔离的话,那么Linux Cgroup则是从资源配额的角度对被管理实体进行了另一个维度的隔离。

举一个例子,分属两个同一层级不同PID Namespace中的进程A和进程B互相不可见,但是它们却都依赖同一个资源池,比如CPU资源池,内存资源池…进程A如果恶意消耗资源,将会饿死进程B,如此一来虽然进程A无法直接 打击 进程B,但是可以通过这种恶意抢占物理资源的方式间接影响进程B,这也是我们所不希望的。Linux Cgroup可以解决这一问题,从资源池利用配额的实现不同Group的完美隔离。

说白了Cgroup机制很简单,就是事先分配一系列的group,然后为每一个group分配一定量的某种资源的配额,然后把进程加入到这些group即可实现资源配额的隔离。

然而现实总是比理论上该如何要考虑更多,首先,我们要对资源进行分类,除了CPU资源外,还有内存,IO等资源,有的策略只想限制进程或者一组进程的CPU利用率,而另外的策略则要限制它们的内存利用率,不一而足,如何设计这个Cgroup的管理结构,就是一个问题了。

Linux内核采用了两层集合多对多的映射方式来解决这个问题,即Cgroup被分成了两类集合:

  • 同类资源( 一个subsystem )被分成Group
  • 不同类资源限额被整合成Set

多个Group的元素和多个Set的元素之间形成了典型的 组相联 结构( 大部分cache均具有的形式 ),最终只要把进程或者多个进程添加进一个Set,即可实现多种资源的配额控制了。相当于 每一个Set从每一个Subsystem中调一个group出来,Subsystem只和Set打交道 。具体如下图所示:

以firejail sandbox解析Docker核心原理依赖的四件套

在很早的时候,2010年,我曾经在北京出差时写过两篇文章来描述有关cgroup的设计:

一个资源管理系统的设计–基于cgroup机制 https://blog.csdn.net/dog250/article/details/5985710

一个资源管理系统的设计–解析linux的cgroup实现 https://blog.csdn.net/dog250/article/details/5991938

那时记得是在做一个WEB UI的元素资源管理系统,也是一个 分组多对多组相联结构 ,于是就想起了Cgroup这个类似的东西,并且直接照抄了Cgroup的设计,显然是提前完成了任务…

好了,来点即视感。

大多数的Linux发行版均已经为你配置好了Cgroup,Linux是通过标准的VFS接口来让用户操作Cgroup的,看看你的/etc/mtab文件里有没有类似下面的内容:

cgroup /sys/fs/cgroup/perf_event cgroup rw,nosuid,nodev,noexec,relatime,perf_event 0 0
cgroup /sys/fs/cgroup/blkio cgroup rw,nosuid,nodev,noexec,relatime,blkio 0 0
cgroup /sys/fs/cgroup/cpuset cgroup rw,nosuid,nodev,noexec,relatime,cpuset 0 0
cgroup /sys/fs/cgroup/net_cls,net_prio cgroup rw,nosuid,nodev,noexec,relatime,net_cls,net_prio 0 0
cgroup /sys/fs/cgroup/freezer cgroup rw,nosuid,nodev,noexec,relatime,freezer 0 0
cgroup /sys/fs/cgroup/cpu,cpuacct cgroup rw,nosuid,nodev,noexec,relatime,cpu,cpuacct 0 0
cgroup /sys/fs/cgroup/devices cgroup rw,nosuid,nodev,noexec,relatime,devices 0 0
cgroup /sys/fs/cgroup/memory cgroup rw,nosuid,nodev,noexec,relatime,memory 0 0
cgroup /sys/fs/cgroup/pids cgroup rw,nosuid,nodev,noexec,relatime,pids 0 0

如果有,那么在/sys/fs/cgroup目录下你便可以操作它了,如果没有,你也可以手工mount它们到任何目录。

以cpu,cpuacct为例,我们来看一个实际的效果。首先准备以下的可执行文件:

root@debian:/home/zhaoya# cat loop.c 
int main()
{
        while(1);// 显然,我们就是想营造一个单CPU利用率100%的场景
}
root@debian:/home/zhaoya# gcc loop.c 
root@debian:/home/zhaoya# ./a.out 

以下是其top视图:

top - 04:55:10 up 10 days, 21:14,  9 users,  load average: 0.70, 0.29, 0.12
Tasks: 156 total,   3 running, 153 sleeping,   0 stopped,   0 zombie
%Cpu0  :  0.0 us,  0.3 sy,  0.0 ni, 99.7 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu1  :  0.0 us,  0.3 sy,  0.0 ni, 99.7 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu2  :100.0 us,  0.0 sy,  0.0 ni,  0.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu3  :  0.3 us,  0.7 sy,  0.0 ni, 99.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  2033804 total,   133816 free,   287248 used,  1612740 buff/cache
KiB Swap:  1046524 total,  1046524 free,        0 used.  1539204 avail Mem 

   PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND                                                                                            
 44902 root      20   0    4040    652    580 R 100.0  0.0   0:58.40 a.out   
...

现在让我们按照下面的步骤配置这个a.out进程到一个单独的Set中:

root@debian:/home/zhaoya# cd /sys/fs/cgroup/cpu
cpu/         cpuacct/     cpu,cpuacct/ cpuset/      
root@debian:/home/zhaoya# cd /sys/fs/cgroup/cpu
root@debian:/sys/fs/cgroup/cpu# 
root@debian:/sys/fs/cgroup/cpu# ls
1                      cgroup.sane_behavior  cpuacct.usage_all         cpuacct.usage_percpu_user  cpu.cfs_period_us  cpu.stat    notify_on_release  tasks
cgroup.clone_children  cpuacct.stat          cpuacct.usage_percpu      cpuacct.usage_sys          cpu.cfs_quota_us   docker      release_agent      user.slice
cgroup.procs           cpuacct.usage         cpuacct.usage_percpu_sys  cpuacct.usage_user         cpu.shares         init.scope  system.slice
root@debian:/sys/fs/cgroup/cpu# 
# 每创建一个目录,相当于创建了一个当前资源type的group
root@debian:/sys/fs/cgroup/cpu# mkdir 10_percent_cpu 
root@debian:/sys/fs/cgroup/cpu#  
root@debian:/sys/fs/cgroup/cpu# ps -e|grep a.out
 44902 pts/6    00:08:56 a.out
# 每echo一个pid到新创建的目录之tasks文件,就相当于将该进程添加到该group,受该group该type资源配额的限制。
# 当echo一个pid到一个group的tasks中时,内核便会建立一个Set,该Set对应引用该group。
root@debian:/sys/fs/cgroup/cpu# echo 44902 >>./10_percent_cpu/tasks 
root@debian:/sys/fs/cgroup/cpu# 
root@debian:/sys/fs/cgroup/cpu# cat ./10_percent_cpu/cpu.cfs_quota_us 
-1
# 指定资源配额,即只能占用10%的CPU资源
root@debian:/sys/fs/cgroup/cpu# echo 10000 >./10_percent_cpu/cpu.cfs_quota_us 
root@debian:/sys/fs/cgroup/cpu# 
# 如果你将另一个a.out进程加入到了该10_percent_cpu这个group,那么两个进程将共享10%的CPU...

此时再看看top统计:

Tasks: 156 total,   3 running, 153 sleeping,   0 stopped,   0 zombie
%Cpu(s):  2.8 us,  0.6 sy,  0.0 ni, 96.6 id,  0.0 wa,  0.0 hi,  0.1 si,  0.0 st
KiB Mem :  2033804 total,   134160 free,   286892 used,  1612752 buff/cache
KiB Swap:  1046524 total,  1046524 free,        0 used.  1539560 avail Mem 

   PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND                                                                                            
 44902 root      20   0    4040    652    580 R  10.2  0.0   9:43.45 a.out

完全符合预期,即便是疯狂的无限循环,CPU利用率也是降到了指定的100%。此时试试再跑一个a.out,然后将其pid加入到/sys/fs/cgroup/cpu//sys/fs/cgroup/cpu,再看看top结果:

PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND                                                                                            
 45758 root      20   0    4040    652    580 R   5.3  0.0   0:22.27 a.out                                                                                              
 44902 root      20   0    4040    652    580 R   4.7  0.0  10:24.90 a.out

就是这么一个意思。对于内存,IO等资源也是完全一样,cgroup甚至可以设置进程或者一组进程跑在固定的cpuset上,非常强大。

那么,也许你没有从上面的即视感操作中看到有那个多对多组相联的视图,Why?事实上,如果一个进程或者一组进程受多种资源限制,就会体现了,比如希望一个进程的CPU利用率不超过10%,内存使用不超过100MB,那么我们不得不创建两个类型的group,在cpu这个type的cgroup mount目录下创建一个10_percent_cpu子目录,然后再在memory这个type的cgroup mount目下创建一个100MB_memory子目录,然后分别将受控进程的pid追加到这两个子目录的tasks文件中。此时的场景就是一个组相联的关系。

对于一个进程而言,它引用多个group中的单独元素,对于一个group而言,它被多个进程引用,为了组织上的高效,内核还使用css_set这个结构体对 引用同一组group元素的task进程了聚合 ,通过hash表来索引,从而提高了内存的空间使用率:

/*
 * A css_set is a structure holding pointers to a set of
 * cgroup_subsys_state objects. This saves space in the task struct
 * object and speeds up fork()/exit(), since a single inc/dec and a
 * list_add()/del() can bump the reference count on the entire cgroup
 * set for a task.
 */
struct css_set {

    /* Reference count */
    atomic_t refcount;

    /*
     * List running through all cgroup groups in the same hash
     * slot. Protected by css_set_lock
     */
    struct hlist_node hlist;

    /*
     * List running through all tasks using this cgroup
     * group. Protected by css_set_lock
     */
    struct list_head tasks;

    /*
     * List of cg_cgroup_link objects on link chains from
     * cgroups referenced from this css_set. Protected by
     * css_set_lock
     */
    struct list_head cg_links;

    /*
     * Set of subsystem states, one for each subsystem. This array
     * is immutable after creation apart from the init_css_set
     * during subsystem registration (at boot time) and modular subsystem
     * loading/unloading.
     */
    // 这里便是存放实际group引用的地方了。
    struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT];

    /* For RCU-protected deletion */
    struct rcu_head rcu_head;
};

不得已的时候我才会贴代码,但是上面的这个结构实在是太重要了。

好了,Linux Cgroup在原理和观感上就是以上这些,Docker无疑使用它做了资源配额的隔离,同理,firejail也有一个cgroup的配置参数,它比Docker使用起来更加简单,如果你对Cgroup有足够的理解,那么即便是在启动Docker或者firejail的时候没有配置Cgroup,事后你也能手工将其限制在某些Cgroup subsystem的group中。

Linux Cgroup就这么多,接着,让我们来看套装中的第三件,OverlayFS!

Linux OverlayFS

OverlayFS必须是一个创举。我这里有几个需求,看看传统的FS如何破解:

1. 我把data1这个分区mount到了/mnt,后来我又把data2分区mount到了/mnt,请问如何在不卸载data2( 服务在运行,无法卸载 )的情况下访问data1里的数据。

2. 两个隔离的容器需要共享相同的几个目录。

3. …

不必说太多,仅仅上面第一个需求就够很多人喝一壶的,至于第二个需求,如果你说把数据复制两份,我自然也没话说,但是问题是,这是最好的方案吗?

OverlayFS可以解决所有的这些问题。

还记得当年的LiveCD吗?你可以在不安装映像的情况下先玩几个小时这个操作系统,这是怎么做到的?用OverlayFS的原理解释起来非常简单。

所谓的OverlayFS就是将既有的FS叠加而成的一种抽象的复合FS,它并不负责底层实际的分区布局和数据存储格式,它只负责 将那些已经存在的FS组合起来 。简单来讲,OverlayFS有4个部分组成:

以firejail sandbox解析Docker核心原理依赖的四件套

  • lower层 :随便的一个目录,在OverlayFS中它是只读的。如果对它进行写入,则会执行Copy-on-Write将其拷贝到upper层之后再写入。
  • upper层 :随便的一个目录,在OverlayFS它是可读写的。
  • merge层 :随便的一个空目录,在OverlayFS中它完成了lower层和upper层的并集,提供一个统一的操作视图。
  • worker空间 :充当工作中的操作数据的空间。

就是上述这么4点关键,可以让OverlayFS任意组合,其中,最为关键的一点就是,lower层可以是OverlayFS的merge层本身,这便使得OverlayFS最终可以递归地任何组合成你想要的任何形式,关键是看你怎么把玩了。

回过头来继续说LiveCD,我们知道光盘是只读的,而内存时可读写的,完全可以将光盘挂载的目录设置为OverlayFS的lower层,然后内存盘的某两个目录作为upper层和merge层,关机期间的临时读写的数据,全部在内存盘里好啦。

值得注意的是,OverlayFS的lower层仅仅在OverlayFS本身的意义上是只读的,即你从merge层视图提供的目录是无法操作lower层的,这并不是lower层本身就一定要是一个只读的文件系统,比如光盘这种,它完全可以是ext4这种,你仍然可以从别的目录去读写lower层的数据。

以软文的形式来描述OverlayFS显得不够专业,那么我就用简单的小实验来获得一点即视感:

首先,我们创建4个目录并且随便放一些文件在lower目录:

root@debian:/home/zhaoya/overlayjail# tree
.
|-- lower
|   `-- lowerfile
|-- merge
|-- upper
`-- worker

4 directories, 1 file

然后我们来mount一个OverlayFS:

root@debian:/home/zhaoya/overlayjail# mount -t overlay overlay -olowerdir=./lower,upperdir=./upper,workdir=./worker ./merge

此时,我们来看下tree的结构:

root@debian:/home/zhaoya/overlayjail# tree
.
|-- lower
|   `-- lowerfile
|-- merge
|   `-- lowerfile
|-- upper
`-- worker
    `-- work

5 directories, 2 files

嗯,确实将lower层的内容合并到merge层了,此时我们尝试修改它:

root@debian:/home/zhaoya/overlayjail# echo abcd >merge/lowerfile 
root@debian:/home/zhaoya/overlayjail# 
root@debian:/home/zhaoya/overlayjail# tree 
.
|-- lower
|   `-- lowerfile
|-- merge
|   `-- lowerfile
|-- upper
|   `-- lowerfile
`-- worker
    `-- work

5 directories, 3 files

可以看到,这个lower层中的lowerfile被拷贝到upper层了,此时如果我们在merge视图下新建一个文件,预期的结果就是该文件会在upper层被创建:

root@debian:/home/zhaoya/overlayjail# touch merge/newfile
root@debian:/home/zhaoya/overlayjail# 
root@debian:/home/zhaoya/overlayjail# tree
.
|-- lower
|   `-- lowerfile
|-- merge
|   |-- lowerfile
|   `-- newfile
|-- upper
|   |-- lowerfile
|   `-- newfile
`-- worker
    `-- work

5 directories, 5 files

符合预期。关于删除文件我就不再解释了,这个请参考其它的资料。接下来我来构建一个嵌套的OverlayFS目录,即用我们刚才的merge层目录merge作为新的lower层来构建:

root@debian:/home/zhaoya/overlayjail# mkdir merge2
root@debian:/home/zhaoya/overlayjail# mkdir upper2
root@debian:/home/zhaoya/overlayjail# 
root@debian:/home/zhaoya/overlayjail# 
root@debian:/home/zhaoya/overlayjail# 
root@debian:/home/zhaoya/overlayjail# mount -t overlay overlay -olowerdir=./merge,upperdir=./upper2,workdir=./worker ./merge2
root@debian:/home/zhaoya/overlayjail# tree
.
|-- lower
|   `-- lowerfile
|-- merge
|   |-- lowerfile
|   `-- newfile
|-- merge2
|   |-- lowerfile
|   `-- newfile
|-- upper
|   |-- lowerfile
|   `-- newfile
|-- upper2
`-- worker
    `-- work

7 directories, 7 files
root@debian:/home/zhaoya/overlayjail# mkdir merge3
root@debian:/home/zhaoya/overlayjail# mount -t overlay overlay -olowerdir=./merge,upperdir=./upper,workdir=./worker ./merge3
root@debian:/home/zhaoya/overlayjail# 
root@debian:/home/zhaoya/overlayjail# 
root@debian:/home/zhaoya/overlayjail# tree
.
|-- lower
|   `-- lowerfile
|-- merge
|   |-- lowerfile
|   `-- newfile
|-- merge2
|   |-- lowerfile
|   `-- newfile
|-- merge3
|   |-- lowerfile
|   `-- newfile
|-- upper
|   |-- lowerfile
|   `-- newfile
|-- upper2
`-- worker
    `-- work

8 directories, 9 files

总之,随便折腾随便整。最终,你可以实现的是,一份数据源,任意方式共享,无需冗余化的复制数据,结合Copy-on-Write,使得资源利用率非常之高。

firejail可以因此获得多少收益呢?让我们重新开始,这次分析一下firejail使用文件系统的方式。

最初你可能会有一个疑问,即我如果仅仅执行了firejail命令,创建了新的Namespace,此时如果我在这个firejail中执行 “rm -rf /” 的话,会发生什么呢?如果真的悲剧了的话,那岂不是firejail枉自称什么jail或者什么sandbox了吗?我还真的这么试了一下,发现我的根文件系统并未损坏,这是怎么做到的呢?

让我们执行firejail的时候加上–debug参数看个究竟:

root@debian:/home/zhaoya# firejail --debug
Autoselecting /bin/bash as shell
Command name #/bin/bash#
Attempting to find server.profile...
Found server profile in /etc/firejail directory
Reading profile /etc/firejail/server.profile
Reading profile /etc/firejail/disable-common.inc
Reading profile /etc/firejail/disable-programs.inc
Reading profile /etc/firejail/disable-passwdmgr.inc

** Note: you can use --noprofile to disable server.profile **

Enabling IPC namespace
Using the local network stack
Parent pid 20238, child pid 20239
The new log directory is /proc/20239/root/var/log
Initializing child process
Host network configured
PID namespace installed
Mounting tmpfs on /run/firejail/mnt directory
# 原来如此!!
Mounting read-only /bin, /sbin, /lib, /lib32, /lib64, /usr, /etc, /var 
Mounting tmpfs on /var/lock
Mounting tmpfs on /var/tmp
Mounting tmpfs on /var/log
Mounting tmpfs on /var/lib/dhcp
Mounting tmpfs on /var/lib/sudo
Mounting tmpfs on /var/cache/apache2
Create the new utmp file
Mount the new utmp file
Mounting a new /home directory
Mounting a new /root directory
Username root, no supplementary groups
Mounting tmpfs on /dev
mounting /run/firejail/mnt/dev/snd directory
mounting /run/firejail/mnt/dev/dri directory
Create /dev/shm directory
Remounting /proc and /proc/sys filesystems
Remounting /sys directory
Disable /sys/firmware
Disable /sys/hypervisor
Disable /sys/module
Disable /sys/power
Disable /sys/kernel/debug
Disable /sys/kernel/vmcoreinfo
Disable /proc/sys/fs/binfmt_misc
Disable /proc/sys/kernel/core_pattern
Disable /proc/sys/kernel/modprobe
Disable /proc/sysrq-trigger
Disable /proc/sys/vm/panic_on_oom
Disable /proc/irq
Disable /proc/bus
Disable /proc/sched_debug
Disable /proc/timer_list
Disable /proc/kcore
Disable /proc/kallsyms
Disable /lib/modules
Disable /usr/lib/debug
Disable /boot
Debug 358: new_name #/tmp/.X11-unix#
Mounting tmpfs on /tmp directory
Whitelisting /tmp/.X11-unix
Disable /etc/xdg/autostart
Disable /etc/X11/Xsession.d
Disable /var/spool/cron
Disable /var/spool/anacron
Disable /run/docker.sock
Disable /etc/cron.hourly
Disable /etc/cron.daily
Disable /etc/cron.weekly
Disable /etc/cron.d
Disable /etc/cron.monthly
Disable /etc/profile.d
Disable /etc/anacrontab
Mounting read-only /root/.bashrc
Disable /etc/shadow
Disable /etc/gshadow
Disable /etc/passwd-
Disable /etc/group-
Disable /etc/shadow-
Disable /etc/gshadow-
Disable /etc/ssh
Disable /bin/umount
Disable /bin/mount
Disable /bin/fusermount
Disable /bin/su
Disable /usr/bin/sudo
Disable /usr/bin/xev
Disable /usr/bin/strace
Disable /bin/nc.traditional
Disable /usr/bin/ncat
Not blacklist /sbin
Not blacklist /usr/sbin
Disable /usr/local/sbin
Disable /tmp/.X11-unix
Disable /sys/fs
...
Dual i386/amd64 seccomp filter configured
SECCOMP Filter:
  VALIDATE_ARCHITECTURE
  EXAMINE_SYSCAL
  UNKNOWN ENTRY!!!
  UNKNOWN ENTRY!!!
  UNKNOWN ENTRY!!!
  BLACKLIST 165 mount
  BLACKLIST 166 umount2
  BLACKLIST 101 ptrace
  BLACKLIST 246 kexec_load
  BLACKLIST 320 kexec_file_load
  BLACKLIST 304 open_by_handle_at
  BLACKLIST 303 name_to_handle_at
  BLACKLIST 175 init_module
  BLACKLIST 313 finit_module
  BLACKLIST 174 create_module
  BLACKLIST 176 delete_module
  BLACKLIST 172 iopl
  BLACKLIST 173 ioperm
  BLACKLIST 251 ioprio_set
  BLACKLIST 167 swapon
  BLACKLIST 168 swapoff
  BLACKLIST 103 syslog
  BLACKLIST 310 process_vm_readv
  BLACKLIST 311 process_vm_writev
  BLACKLIST 139 sysfs
  BLACKLIST 156 _sysctl
  BLACKLIST 159 adjtimex
  BLACKLIST 305 clock_adjtime
  BLACKLIST 212 lookup_dcookie
  BLACKLIST 298 perf_event_open
  BLACKLIST 300 fanotify_init
  BLACKLIST 312 kcmp
  BLACKLIST 248 add_key
  BLACKLIST 249 request_key
  BLACKLIST 250 keyctl
  BLACKLIST 134 uselib
  BLACKLIST 163 acct
  BLACKLIST 154 modify_ldt
  BLACKLIST 155 pivot_root
  BLACKLIST 206 io_setup
  BLACKLIST 207 io_destroy
  BLACKLIST 208 io_getevents
  BLACKLIST 209 io_submit
  BLACKLIST 210 io_cancel
  BLACKLIST 216 remap_file_pages
  BLACKLIST 237 mbind
  BLACKLIST 239 get_mempolicy
  BLACKLIST 238 set_mempolicy
  BLACKLIST 256 migrate_pages
  BLACKLIST 279 move_pages
  BLACKLIST 278 vmsplice
  BLACKLIST 161 chroot
  BLACKLIST 184 tuxcall
  BLACKLIST 169 reboot
  BLACKLIST 180 nfsservctl
  BLACKLIST 177 get_kernel_syms
  RETURN_ALLOW
Save seccomp filter, size 880 bytes
Username root, no supplementary groups
starting application
LD_PRELOAD=(null)
Running /bin/bash command through /bin/bash
execvp argument 0: /bin/bash
execvp argument 1: -c
execvp argument 2: /bin/bash
Child process initialized
monitoring pid 3

root@debian:~# ps -e
   PID TTY          TIME CMD
     1 ?        00:00:00 firejail
     3 ?        00:00:00 bash
     8 ?        00:00:00 ps

办法也是够low的了吧,为了防止误伤根文件系统,这么多的条条框框…现在当我们知道了有了OverlayFS这个文件系统后,一切防误伤的机制完全交给OverlayFS即可,我们只需要准备一个可以chroot的最小化映像目录,然后将其设置为其lower层即可。

总之就是, 我们把不希望被完全修改的文件或者多个firejail用户共享的文件放进lower层,其它的比如私有文件放进upper层,然后mount到一个merge层,最终让firejail通过命令行参数chroot到那个merge目录就妥妥的了

如果在这一小节说完了全部,那么本文后面的内容就显得空洞了,所以我还是准备把合并成Docker样子的firejail最后的一点内容再往后拖拖。拖到 “合并为Docker” 那个小节。

Linux虚拟网卡

本节篇幅不会太多,详情看下面的文章:

图解几个与Linux网络虚拟化相关的虚拟网卡-VETH/MACVLAN/MACVTAP/IPVLAN https://blog.csdn.net/dog250/article/details/45788279

Linux虚拟网卡可以说是完成了Net Namespace网络协议栈隔离的下端的最后一小段。最终的网络协议栈隔离效果成了下面的形式:

以firejail sandbox解析Docker核心原理依赖的四件套

也就是说,对于firejail而言,它的–net参数除了新开辟一个独立的Net Namespace之外,还会在参数指定的物理网卡上创建一个macvlan虚拟接口供该firejail使用。

合并为Docker

当我安装好Docker的基础映像debian之后,我死死盯住Docker基础映像的目录:

/var/lib/docker/overlay2/29ee7bc2bce7accafd0ad4442d04810f89f1c613d7b98c49ad632d36b2c53b06/diff/

root@debian:/home/zhaoya# tree /var/lib/docker/overlay2/29ee7bc2bce7accafd0ad4442d04810f89f1c613d7b98c49ad632d36b2c53b06/diff/ |more
/var/lib/docker/overlay2/29ee7bc2bce7accafd0ad4442d04810f89f1c613d7b98c49ad632d36b2c53b06/diff/
|-- bin
|   |-- bash
|   |-- cat
|   |-- chgrp
|   |-- chmod
...
|   |-- gunzip
|   |-- gzexe
|   |-- gzip
|   |-- hostname
|   |-- ip
|   |-- ln
...

然后告诉自己,这个目录里就是一个基础可chroot运行的 根文件系统 。这种小的基础根文件系统在一些小型嵌入式设备上非常之常见,比如家用路由器什么的。以前我自己也做过这个,比如busybox什么的,但是Docker的基础根可能会重一些,总之呢,细节不管了,只要认定它可以作为OverlayFS的lower层就行。

接下来我们在任意目录下构建下面的结构:

root@debian:/home/zhaoya/overlayjail# tree
.
|-- merge
|-- upper
`-- worker
    `-- work

4 directories, 0 files

这个就是我的容器目录了。接下来,我们来mount一个OverlayFS:

root@debian:/home/zhaoya/overlayjail# mount -t overlay overlay -olowerdir=/var/lib/docker/overlay2/29ee7bc2bce7accafd0ad4442d04810f89f1c613d7b98c49ad632d36b2c53b06/diff/,upperdir=./upper,workdir=./worker ./merge
root@debian:/home/zhaoya/overlayjail# 
root@debian:/home/zhaoya/overlayjail# 
root@debian:/home/zhaoya/overlayjail# cd merge/
root@debian:/home/zhaoya/overlayjail/merge# ls
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
root@debian:/home/zhaoya/overlayjail/merge# cd..

确认无误后,便可以firejail了。

root@debian:/home/zhaoya/overlayjail# firejail --net=enp0s17 --chroot=./merge 
Warning: default profile disabled by --chroot option
Parent pid 22345, child pid 22346
The new log directory is /proc/22346/root/var/log
Warning: failed to unmount /sys
Warning: whitelist feature is disabled in chroot

Interface        MAC                IP               Mask             Status
lo                                  127.0.0.1        255.0.0.0        UP    
eth0-22345       06:42:fd:88:3f:25  192.168.44.206   255.255.255.0    UP    
Default gateway 192.168.44.2

Child process initialized
root@debian:~# ping www.baidu.com
PING www.a.shifen.com (14.215.177.38) 56(84) bytes of data.
64 bytes from 14.215.177.38 (14.215.177.38): icmp_seq=1 ttl=128 time=53.9 ms
64 bytes from 14.215.177.38 (14.215.177.38): icmp_seq=2 ttl=128 time=10.0 ms
^C
--- www.a.shifen.com ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 10.075/31.988/53.902/21.914 ms
root@debian:~# 
root@debian:~# 
root@debian:~# apt-get update
Get:1 http://security.debian.org/debian-security stretch/updates InRelease [94.3 kB]
...
Fetched 7810 kB in 23s (334 kB/s)                                                                                                                                      
Reading package lists... Done
root@debian:~# apt-get install  apache2
Reading package lists... Done
Building dependency tree       
Reading state information... Done
The following additional packages will be installed:
  apache2-bin apache2-data apache2-utils bzip2 file libapr1 libaprutil1 libaprutil1-dbd-sqlite3 libaprutil1-ldap libexpat1 libffi6 libgdbm3 libgmp10 libgnutls30
  libgpm2 libhogweed4 libicu57 libldap-2.4-2 libldap-common liblua5.2-0 libmagic-mgc libmagic1 libncurses5 libnghttp2-14 libp11-kit0 libperl5.24 libprocps6 libsasl2-2
  libsasl2-modules libsasl2-modules-db libsqlite3-0 libssl1.0.2 libssl1.1 libtasn1-6 libxml2 mime-support netbase openssl perl perl-modules-5.24 procps psmisc rename
  sgml-base ssl-cert xml-core xz-utils
Suggested packages:
  www-browser apache2-doc apache2-suexec-pristine | apache2-suexec-custom bzip2-doc gnutls-bin gpm libsasl2-modules-gssapi-mit | libsasl2-modules-gssapi-heimdal
  libsasl2-modules-ldap libsasl2-modules-otp libsasl2-modules-sql ca-certificates perl-doc libterm-readline-gnu-perl | libterm-readline-perl-perl make sgml-base-doc
  openssl-blacklist debhelper
The following NEW packages will be installed:
  apache2 apache2-bin apache2-data apache2-utils bzip2 file libapr1 libaprutil1 libaprutil1-dbd-sqlite3 libaprutil1-ldap libexpat1 libffi6 libgdbm3 libgmp10
  libgnutls30 libgpm2 libhogweed4 libicu57 libldap-2.4-2 libldap-common liblua5.2-0 libmagic-mgc libmagic1 libncurses5 libnghttp2-14 libp11-kit0 libperl5.24
  libprocps6 libsasl2-2 libsasl2-modules libsasl2-modules-db libsqlite3-0 libssl1.0.2 libssl1.1 libtasn1-6 libxml2 mime-support netbase openssl perl perl-modules-5.24
  procps psmisc rename sgml-base ssl-cert xml-core xz-utils
0 upgraded, 48 newly installed, 0 to remove and 0 not upgraded.
Need to get 24.8 MB of archives.
After this operation, 104 MB of additional disk space will be used.
Do you want to continue? [Y/n] y
Get:1 http://security.debian.org/debian-security stretch/updates/main amd64 perl-modules-5.24 all 5.24.1-3+deb9u4 [2724 kB]
...
Enabling conf serve-cgi-bin.
Enabling site 000-default.
invoke-rc.d: could not determine current runlevel
invoke-rc.d: policy-rc.d denied execution of start.
Processing triggers for libc-bin (2.24-11+deb9u3) ...
Processing triggers for sgml-base (1.29) ...
root@debian:~# 
root@debian:~# 
root@debian:~# service apache2 start
root@debian:~# ss -lnt
State      Recv-Q Send-Q                                       Local Address:Port                                                      Peer Address:Port              
LISTEN     0      128                                                     :::80                                                                  :::*

好了,Apache已经在我们的firejail中运行了,此时,我要停止它的运行,然后在接下来的某个时间再次运行我这个已经配置好的Apache…或者说将它迁移到别的机器上运行,好了,先停止:

root@debian:~# 
root@debian:~# echo "OK! This is my firejail image of HTTPd"
OK! This is my firejail image of HTTPd
root@debian:~#  
root@debian:~# echo "OK! This is my firejail image of HTTPd" >/logo
root@debian:~# 
root@debian:~# cat /logo 
OK! This is my firejail image of HTTPd
root@debian:~# 
root@debian:~# exit
exit

Parent is shutting down, bye...
root@debian:/home/zhaoya/overlayjail#

此时我们看一下现在的upper目录,它已经被firejail里面的apt-get install填满了东西:

root@debian:/home/zhaoya/overlayjail# tree upper |more
upper
|-- bin
|   |-- aa
|   |-- bunzip2
...
|   |-- kill
|   `-- ps
|-- etc
|   |-- alternatives
|   |   |-- editor -> /usr/bin/vim.basic
|   |   |-- editor.1.gz -> /usr/share/man/man1/vim.1.gz
|   |   |-- editor.fr.1.gz -> /usr/share/man/fr/man1/vim.1.gz
...
|   |   |-- vi.ru.1.gz -> /usr/share/man/ru/man1/vim.1.gz
|   |   |-- view -> /usr/bin/vim.basic
|   |   |-- view.1.gz -> /usr/share/man/man1/vim.1.gz
...
|   |   |-- vim -> /usr/bin/vim.basic
|   |   |-- vimdiff -> /usr/bin/vim.basic
|   |   |-- w -> /usr/bin/w.procps
|   |   `-- w.1.gz -> /usr/share/man/man1/w.procps.1.gz
|   |-- apache2
|   |   |-- apache2.conf
|   |   |-- conf-available
|   |   |   |-- charset.conf
|   |   |   |-- localized-error-pages.conf
|   |   |   |-- other-vhosts-access-log.conf
|   |   |   |-- security.conf
|   |   |   `-- serve-cgi-bin.conf
|   |   |-- conf-enabled
|   |   |   |-- charset.conf -> ../conf-available/charset.conf
...
|   |   |-- envvars
|   |   |-- magic
|   |   |-- mods-available
|   |   |   |-- access_compat.load
|   |   |   |-- actions.conf
...

过了很久之后,我们再次运行firejail:

root@debian:/home/zhaoya/overlayjail# firejail --net=enp0s17 --chroot=./merge
Warning: default profile disabled by --chroot option
Parent pid 24765, child pid 24766
The new log directory is /proc/24766/root/var/log
Warning: failed to unmount /sys
Warning: whitelist feature is disabled in chroot

Interface        MAC                IP               Mask             Status
lo                                  127.0.0.1        255.0.0.0        UP    
eth0-24765       fa:41:e9:f3:ff:4a  192.168.44.177   255.255.255.0    UP    
Default gateway 192.168.44.2

Child process initialized
root@debian:~# 
root@debian:~# 
root@debian:~# cat /logo 
OK! This is my firejail image of HTTPd #确实还是它!这是上次退出前打入的记号。
root@debian:~# 

因此,我知道,我只需要将upper目录打包迁移即可,我自己安装配置的东西全部都在这个可读写的upper层,至于说lower层的 /var/lib/docker/overlay2/29ee7bc2bce7accafd0ad4442d04810f89f1c613d7b98c49ad632d36b2c53b06/diff/ ,这个一般是公共的,共享的,它在一个公共的目录下可以被很容易获取到!

我只需要把upper目录想个办法打包带走就好了,至于采用什么格式,是否压缩,那就是另外的问题了。总之,我可以把upper目录看作是一个标准化的集装箱了,把它无论搬到哪里,你只需要下面的命令就能让里面已经配置好的服务开始运行:

1. 准备Base_Image & 挂载OverlayFS

mkdir worker
mkdir merged
mv my_upper ./upper
mount -t overlay overlay -olowerdir=Public_Base_Image,upperdir=./upper,workdir=./worker/ ./merged

2.创建新的PID Namespace以及Net Namespace

firejail --chroot=./merged/ --net=eth0

哦,对了,好像忘了cgroup,嗯,这是我故意的,因为它比较简单,留作练习吧:

–cgroup=tasks-file   Place the sandbox in the specified control group. tasks-file is the full path of cgroup tasks file.   .   Example:   # firejail –cgroup=/sys/fs/cgroup/g1/tasks

只需要添加–cgroup参数即可,至于cgropu怎么创建,却是另一个话题了。

到此为止,我用一个简单的firejail命令行 工具 实现了一个 可以随意带走任意发布的容器 ,但是这一切看起来或者用起来并不是特别方便。是的,确实不方便,但是我要说的不是 如何把这一系列的手工操作步骤通过优化和封装变得方便起来 ,事实上,关于firejail也几乎就到此为止了。

我要说的是,Docker的底层使用了几乎一模一样的机制,我只是通过firejail而不是Docker本身阐释了Docker底层的支撑技术,即所谓的Linux内核容器四件套,也正如本文的题目中所指示的那般。

那么,Docker有什么不同?既然在底层机制看来,它完全就可以用firejail搭建而成,那么成就它伟大的特点是什么?是什么导致了是Docker而不是firejail获得了几乎所有人的青睐?

关于这个问题的解释,是Docker最为精彩的地方,然而很遗憾,这不是本文的主题,本文的主题是 一些所有类似的容器底层固定不变的东西

简单说一下我的观点。

虽然很多人把Docker看作是集装箱,集装箱确实是一个伟大的创举,统一整齐划一的操作,从工人的工作服安全帽到叉车,卡车,机械手臂都是统一的标准,确实伟大。所以,Docker类比集装箱说明Docker同样伟大,这毋庸置疑。

但是,其实还有一个同样伟大的东西,那就是外卖盒子( together with送餐员的外卖箱子 )。你不觉得吗?所以说,我手工用firejail完成了一个类似Docker的可以任意发布的东西之后,我更倾向于将其叫做 可以随意带走的 东西,即take away food,如果将这个过程再标准化一点,就是 外卖 了。

在体量上,我承认同样方式打包的隆江猪脚饭,麻辣烫,鸭血粉丝汤绝对比不上上海洋山港的那些自如游走的集装箱,但是大多数人更关注哪些呢?

说到微服务,说到微信小程序,比如还有之前很久的Java Applet,如今的Docker,大家都在强调部署和发布的简单易操作,最终Docker似乎完成了所有的事,将环境,配置,二进制完全整合在一个映像里,然后发布,任意运行。非常不错,但是千万不要把它看作是集装箱那么高大上的东西,我觉得看作是外卖盒子不错。

怎么说呢…既然你想把你的二进制,服务,配置,环境整合成一种普通人唾手可得的打包品,take away food,何不去向外卖而不是港口去取取经呢?说个猛的,你觉得外卖只是送隆江猪脚饭,麻辣烫吗?其实外卖还可以送烤鱼和火锅,不信吧,你叫个海底捞试试?

Docker组网模型

说起组网模型,还记得VMWare虚拟机的组网模型吗?四种模式:

  • Bridge模式
  • Host only模式
  • NAT模式
  • Segment模式

具体可以参见我下面的文章:

深入浅出VMware的组网模式 https://blog.csdn.net/dog250/article/details/7363534

年代久远的一篇总结。这里要说的并不是旧事重提,而是说,Docker的组网模型要远比VMWare还要花式。这里说几个有特点的:

Bridge模式

这是最简单的模式,请看下图:

以firejail sandbox解析Docker核心原理依赖的四件套

MACVLAN模式

这个比较类似firejail的隔离模式,使用将物理网卡创建多个MACVLAN虚拟网卡来组网:

以firejail sandbox解析Docker核心原理依赖的四件套

Container模式

Container模式比较有意思。它实际上是将整个Net Namespace和虚拟网卡作为一个 组件 来使用,这个完整的协议栈是可插拔的。

以firejail sandbox解析Docker核心原理依赖的四件套

有的时候,我们会对多个容器配置完全相同的网络策略,比如完全相同的iptables,完全相同的路由表,这个时候,我们可以通过OverlayFS共享配置文件的方式轻松搞定,但有的时候,如果容器的网络策略需要和容器本身隔离的时候,应该怎么办呢?比如网络策略是具有独立权限的人事先配好在宿主机的某个Net Namespace中的时候,也就是说,它根本不属于容器。这个时候就需要容器的网络配置可以Attach其它的Net Namespace。

最重要的两件套

如果问Docker的运行依赖的东西中,哪些是必不可少的,回答无疑是两个,即Namespace和OverlayFS:

  • Namespace:提供了进程时间维度的隔离和复用;
  • OverlayFS:提供了空间维度的隔离( 容器layer,即upper )和复用( 基础映像,即lower )。

本来,容器这种东西就是要比虚拟机更加轻量的,但至于轻量到什么程度,Docker比较好的定义了一个下界,不能比这个更加轻量了。当然,不考虑Docker在应用部署,服务发布,应用编排,服务迁移上的那些优势,仅仅从底层的角度来讲,firejail定义的下界似乎更加合理,它连OverlayFS都没有强制使用,但是Docker却不行,毕竟从使用者的角度来讲,Docker之所以为Docker,之所以是Docker而不是firejail,原因无疑也就是它的那些 应用部署,服务发布,应用编排,服务迁移 上优势使然,而构建这些优势的根基,离不开OverlayFS!

至于另外两个,Cgroup以及Linux虚拟网卡,则是两个锦上添花的东西。

后记

写这篇文章前,我其实是想起了一件事。那还是在四五年前吧,我很迷茫。我这个人遇到困难或者迷茫的时候,绝对不会隐瞒,我会直接告诉我的家人,同事以及我的直属领导寻求帮助,目的是及时止损,而不是到最后害人害己。

我约了我的领导谈话,我说我项目结束后就不知道该干嘛了,非常无聊。然后我们就扯了很多家常,我很清晰的记得他在说他自己的时候,他说他比较擅长和喜欢的是用已有的东西构建出一个新东西,而不是造轮子。简单说吧,就是搭积木,各种玩法,任意组合。这一点直击心灵。

我也喜欢干这个,小的时候我就喜欢玩变形金刚,偷买过无数的版本,后来就喜欢各种机械物件,动手搞过很多小东西,也浪费过不少财物…后来学了点编程,这种可以用廉价的电力作为微小的代价无限试错的东西…

最终,在听了我那领导的话后,我决定把所有我会的那些复杂的东西,用简单的小东西重新搭建一遍,这完全是UNIX的风格,这个我早就知道,我也不是为了去印证什么,其实就是为了打发无聊,我知道过程中肯定会遇到很多的问题,解决这些问题的过程本身就能获得一些快感,直到我遇到下一个有挑战性的项目或者离开我现在的工作环境。

千岩万转路不定,迷花倚石忽已暝。

熊咆龙吟殷岩泉,浙江温州皮鞋湿。

Pending:firejail的macvlan用的是什么模式呢?是否合理呢?留作思考题。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

JavaScript Patterns

JavaScript Patterns

Stoyan Stefanov / O'Reilly Media, Inc. / 2010-09-21 / USD 29.99

What's the best approach for developing an application with JavaScript? This book helps you answer that question with numerous JavaScript coding patterns and best practices. If you're an experienced d......一起来看看 《JavaScript Patterns》 这本书的介绍吧!

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具