以firejail sandbox解析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  # 启动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   04566 -      19:16 ?        00:00:00 firejail0 S root          3      1  0  80   05285 -      19:16 ?        00:00:00 /bin/bash0 R root         20      3  0  80   09576 -      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   04566 -      19:16 ?        00:00:00 firejail0 S root          3      1  0  90  105285 -      19:16 ?        00:00:00 /bin/bash # 已然改变!0 R root         21      3  0  90  109576 -      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,然后重新启动,这次携带一个参

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值