摘要:之所以称这次代码阅读为奇幻之旅,是因为作者刘梦馨在阅读Docker源码的过程中,发现了几个有趣的事情:从代码来看Docker并未另起炉灶,而是将现有经过的隔离安全机制能用的全用上,但源代码的质量让人很难恭维。
编者按:前段时间转载的《5分钟弄懂Docker!》很受欢迎,短短1500字,让大家快速了解了Docker。今天看到作者又出了新作,马上就转过来了。之所以称这次代码阅读为奇幻之旅,是因为作者刘梦馨(@oilbeater)在阅读Docker源码的过程中,发现了几个有趣的事情:从代码来看 Docker 并没有另起炉灶新开发机制,而是将现有经过的隔离安全机制能用的全用上,包括 cgroups,capability,namespaces,AppArmor 和 SELinux(CSDN刚刚翻译的文章《容器VS虚拟化之安全》中也提到了这一点);从读代码的角度来看,Docker源代码的质量让人很难恭维,一些代码风格和逻辑上让人读起来很费劲。
以下为原文:
一直对Docker提供的容器感到好奇,不知道究竟是如何实现隔离和安全的,之前Docker本来是用LXC来提供容器功能的,但是由于对内核代码有一丝恐惧没敢去看,后来听说Docker为了实现跨平台兼容自己实现了一套Native的容器就是libcontainer。既然是新项目那么代码量和复杂度应该都不会太高吧,抱着这个想法我就翻看libcontainer的代码读一读。
首先自然要下到代码才能读,去下完整的Docker源码,不要只下libcontainer的源码。不然就会像我一样读的时候碰到一个坑,掉里面爬了半天。
接下来就要有一个代码阅读器了,由于Go语言还是个比较新的语言,配套的工具还不是很完善,不过可以用liteide(自备梯子)这个轻量级的Golang IDE来兼职一下。
打开之后可以看到Docker的目录结构大致是这样的:
那么我们所关注的libcontainer在哪里呢?藏得还挺深的在\libcontainer\。进去之后就会发现有个显眼的container.go在向你招手,嗯第一个坑马上就要来了。
可以看出这段代码只是定义了一个接口,任何实现这些方法的对象就会变成一个Docker认可的 container。其中比较关键的一个函数就是Start了,他是在container里启动进程的方法,可以看到接口的要求是传进一个所要启动进程相关的配置,返回一个进程pid和一个接受退出信息的channel。
下一步自然就是去找这个接口的实现,看看究竟是怎么做的,然后一个坑就来了。由于Go语言不要求对象向Java那样显性声明自己实现哪个接口,只要自己默默实现了对应的方法,就默认变成了那个接口类型的对象。所以没有什么直观的方法,来找到哪些对象实现了这个接口,翻了一下libcontainer文件夹下的文件感觉哪个都不像。感觉有些不详的预兆,装了个Cygwin去grep Start这个函数,结果意外的发现没有,于是又在整个Docker目录下去 grep 发现还是没有。
我就奇怪了,不是说Docker 1.2之后就支持Native的container了么,他连libcontainer里的container接口都没实现,是怎么调用Native的container的。既然自底向上的找不到,那就只能自顶向下的从上层往下跟去找找怎么回事了。
其中和进程隔离相关的有 Resources 了 CPU 和 memory 的资源分配,可供 cgroups 将来调用。 CapAdd 和 CapDrop 这个和 Linux Capability 相关来控制 root 的某些系统调用权限不会被容器内的程序使用。ProcessLabel 为容器内的进程打上一个 Lable 这样的话 SELinux 将来就可以通过这个 lable 来做权限控制。Apparmorprofile 指向 Docker 默认的 apparmor profile 径,一般为/etc/apparmor.d/docker,用来控制程序对文件系统的访问权限。
可以看到,Docker 对容器的隔离策略并不是自己开发一套隔离机制,而是把现有的能用的已有隔离机制全用上。甚至 AppArmor 和 SELinux 这两个类似并且人家两家还在相互竞争的机制也都一股脑不管三七二十一全加上,颇有拿来主义的风采。这样的话万一恶意程序突破了一层防护还有另外一层挡着,而且这几个隔离机制还相互要同时突破所有的防护才行。
而我们真正要在容器中执行的程序在 ProcessConfig 这个结构体中的 Entrypoint。由此可见所谓的容器就是一个穿着各种隔离外套的程序,用这些隔离外套这个程序可以活在自己的小天地里,不知有汉无论魏晋。
不太确定一个函数 8 个参数真的好么,但是我更纳闷的是在主项目里既然都有 pipe 这个结构把 stdin,stdout,stderr 放在一起为啥到这里就要分开写了,6 个虽然也不少,但是比 8 个要好点。回过头来说一下 namespace ,这又是另一种隔离机制。顾名思义,隔离的是名字空间,这要的话本来属于全局可见的名字资源,如 pid,network,mountpoint 之类的资源虚拟出多份,每个 namespace 一份,每组进程占用一个 namespace。这样的话容器内程序都看不到外部其他进程,的难度自然也就加大了。
然后这里面最关键的执行的一句倒是很简单了。
if err := command.Start(); err != nil { child.Close() return -1, err}
其中的 command 是系统调用类 exec.Cmd 的一个对象,而之前的关于程序的配置信息已经在那个一行的执行代码里都整合进 command 里了,在这里只要 start 一下程序就跑起来了。然后我就疑惑了,这个函数不是 namespaces 包下的么,咋没有 namespaces 设置的相关代码呢。其实你仔细看那一行的执行代码可以发现 namespaces 的设置也在里面了,换句话说这个 namespaces 包下的 exec 其实没有做什么和 namespaces 相关的事情,只是 start 了一下。这种代码逻辑结构可是给读代码的人带来了不小的困惑啊。
这次读代码的起点是想搞懂容器是如何做隔离和安全的。从代码来看 Docker 并没有另起炉灶新开发机制,而是将现有经过的隔离安全机制能用的全用上,包括 cgroups,capability,namespaces,AppArmor 和 SELinux。这样一套组合拳打出来的效果理论上看还是很好的,即使其中一个机制出了漏洞,但是要利用这个漏洞的方法很可能会被其他机制住,要找到一种同时绕过所有隔离机制的方法难度就要大多了。
但是从读代码的角度来看,Docker 的代码的质量就让人很难恭维了,即使 libcontainer 是一个的部分,但本是同根生的名字都不一致,不知道之后会不会更混乱。而一些代码风格和逻辑上也实在让人读起来很费劲,代码质量要提高的地方还有很多。毕竟是开源的项目,即使功能很强大,但是大家如果发现代码质量有问题,恐怕也不大敢用在生产吧。
而至于 libcontainer 尽管从 Docker 中出去发展,但是可以看出和主项目还有一些没有切分干净的地方,而且 Docker 主项目目前也没有采用 libcontainer 中的 container 方式,只是在调用里面的一些机制方法,看样子目前还处于一个逐步替换的过程中。libcontainer 和一个完整的产品还有一段距离,诸位有兴趣的也可以参与进去,万一这就是下一个伟大的项目呢?
原文链接:一次奇幻的 docker libcontainer 代码阅读之旅(责编:周小璐)
如需要了解更多Docker相关的资讯或是技术文档可访问Docker技术社区;如有更多的疑问请在提出,我们会邀请专家回答。CSDN Docker技术交流。
网友评论 ()条 查看