偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

一次奇幻的 docker libcontainer 代碼閱讀之旅

云計算
一直對 docker 提供的容器感到好奇,不知道究竟是如何實現(xiàn)隔離盒保證安全的,之前 docker 本來是用 lxc 來提供容器功能的,但是由于對內(nèi)核代碼有一絲恐懼沒敢去看,后來聽說 docker 為了實現(xiàn)跨平臺兼容自己實現(xiàn)了一套 native 的容器就是 libcontainer 。既然是新項目那么代碼量和復(fù)雜度應(yīng)該都不會太高吧,抱著這個想法我就翻看 libcontainer 的代碼讀一讀。

準(zhǔn)備工作

首先自然要下到代碼才能讀,建議去下完整的 docker 源碼,不要只下 libcontainer 的源碼。不然就會像我一樣讀的時候碰到一個坑掉里面爬了半天。

接下來就要有一個代碼閱讀器了,由于 go 語言還是個比較新的語言,配套的工具還不是很完善,不過可以用 liteide (自備梯子)這個輕量級的 golang ide 來兼職一下。

打開之后可以看到 docker 的目錄結(jié)構(gòu)大致是這樣的。

alt

那么我們所關(guān)注的 libcontainer 在哪里呢?藏得還挺深的在 \verdor\src\github.com\libcontainer\。進(jìn)去之后就會發(fā)現(xiàn)有個顯眼的 container.go 在向你招手,嗯第一個坑馬上就要來了。
container

這段代碼初看起來還是很淺顯的。代碼縮水后如下

 

  1. type Container interface { 
  2. ID() string 
  3. RunState() (*RunState, Error) 
  4. Config() *Config 
  5. Start(config *ProcessConfig) (pid int, exitChan chan int, err Error) 
  6. Destroy() Error 
  7. Processes() ([]int, Error) 
  8. Stats() (*ContainerStats, Error) 
  9. Pause() Error 
  10. Resume() Error 

可以看出這段代碼只是定義了一個接口,任何實現(xiàn)這些方法的對象就會變成一個 docker 認(rèn)可的 container。其中比較關(guān)鍵的一個函數(shù)就是 Start 了,他是在 container 里啟動進(jìn)程的方法,可以看到接口的要求是傳進(jìn)一個所要啟動進(jìn)程相關(guān)的配置,返回一個進(jìn)程 pid 和一個接受退出信息的 channel。

下一步自然就是去找這個接口的實現(xiàn)去看看究竟是怎么做的,然后一個坑就來了。由于 go 語言不要求對象向 java 那樣顯示的聲明自己實現(xiàn)哪個接口,只要自己默默實現(xiàn)了對應(yīng)的方法就默認(rèn)變成了哪個接口類型的對象。所以沒有什么直觀的方法來找到哪些對象實現(xiàn)了這個接口,翻了一下 libcontainer 文件夾下的文件感覺哪個都不像。感覺有些不詳?shù)念A(yù)兆,裝了個 Cygwin 去 grep Start 這個函數(shù),結(jié)果意外的發(fā)現(xiàn)沒有,于是又在整個 docker 目錄下去 grep 發(fā)現(xiàn)還是沒有。

我就奇怪了,不是說 docker 1.2 之后就支持 native 的 container 了么,他連 libcontainer 里的 container 接口都沒實現(xiàn)他是怎么調(diào)用 native 的 container 的。既然自底向上的找不到,那就只能自頂向下的從上層往下跟去找找怎么回事了。
driver

docker 支持 lxc 和 native 兩套容器實現(xiàn),是通過 driver 這個接口的兩個實現(xiàn)來完成的。在 \daemon\execdriver 中可以看到有 lxc 和 native 兩個文件夾,里面就是相關(guān)的代碼。不過在 \daemon\ 目錄下可以看到還有一個 container.go 里面是有個 container 對象,可是并沒有實現(xiàn) libcontainer 里對應(yīng)的接口,難道 libcontainer 里的那個 interface 只是一個幌子?

先看一下 driver 這個接口

 

  1. type Driver interface { 
  2. Run(c *Command, pipes *Pipes, startCallback StartCallback) (int, error) // Run executes the process and blocks until the process exits and returns the exit code 
  3. // Exec executes the process in a running container, blocks until the process exits and returns the exit code 
  4. Exec(c *Command, processConfig *ProcessConfig, pipes *Pipes, startCallback StartCallback) (int, error) 
  5. Kill(c *Command, sig int) error 
  6. Pause(c *Command) error 
  7. Unpause(c *Command) error 
  8. Name() string // Driver name 
  9. Info(id string) Info // "temporary" hack (until we move state from core to plugins) 
  10. GetPidsForContainer(id string) ([]int, error) // Returns a list of pids for the given container. 
  11. Terminate(c *Command) error // kill it with fire 
  12. Clean(id string) error// clean all traces of container exec 

有沒有感覺名字雖說和上面的 container interface 不太一樣,不過意思是差不多的。resume 變成了 unpause, destory 變成了 teminate,processes 變成了 getpidsforcontainer,start 也變成了 run 和 exec 兩個函數(shù)??吹竭@不得不說 docker 的代碼的一致性和可讀性還是慘了點,codereview 需要更嚴(yán)格一些呀。

再進(jìn)到 native 的 driver.go 就可以看到具體的實現(xiàn)了。在文件頭部發(fā)現(xiàn)了一長串 import,其中有幾個比較抓眼球:

 

  1. import ( 
  2. .... 
  3. "github.com/docker/libcontainer" 
  4. "github.com/docker/libcontainer/apparmor" 
  5. "github.com/docker/libcontainer/cgroups/fs" 
  6. "github.com/docker/libcontainer/cgroups/systemd" 
  7. consolepkg "github.com/docker/libcontainer/console" 
  8. "github.com/docker/libcontainer/namespaces" 
  9. "github.com/docker/libcontainer/namespaces/nsenter" 
  10. "github.com/docker/libcontainer/system" 

從這里似乎可以看出一點端倪了。libcontainer 的目的是提供一個平臺無關(guān)的原生容器,這需要包括資源隔離,權(quán)限控制等一系列通用組件,所以 libcontainer 就來提供這些通用組件,所以他叫 "lib"。而每個平臺想實現(xiàn)自己的容器的話就可以借用這些組件,當(dāng)然可以只用一部分而不全用, docker 就相當(dāng)于用了包括 apparmor、cgroups、namespaces 等等組件,然后沒用 libcontainer 的 container 接口和其他一些組件,自己寫了其他部分完成的所謂 native 的容器。

還是看 run 函數(shù)

 

  1. func (d *driver) Run(c *execdriver.Command, pipes *execdriver.Pipes, startCallback execdriver.StartCallback) (int, error)  

其中 execdriver.Pipes 是一個定義標(biāo)準(zhǔn)輸入輸出和錯誤指向的結(jié)構(gòu),startCallback 是在進(jìn)程結(jié)束或者退出時調(diào)用的一個回調(diào)函數(shù),最重要的結(jié)構(gòu)是 execdriver.Command 他定義了容器內(nèi)運行程序的各種環(huán)境和約束條件??梢栽?daemon 下的 driver.go 中找到對應(yīng)的定義。
Command

 

  1. type Command struct { 
  2. ID string `json:"id"
  3. Rootfs string `json:"rootfs"// root fs of the container 
  4. InitPath string `json:"initpath"// dockerinit 
  5. WorkingDir string `json:"working_dir"
  6. ConfigPath string `json:"config_path"// this should be able to be removed when the lxc template is moved into the driver 
  7. Network *Network `json:"network"
  8. Resources *Resources `json:"resources"
  9. Mounts []Mount `json:"mounts"
  10. AllowedDevices []*devices.Device `json:"allowed_devices"
  11. AutoCreatedDevices []*devices.Device `json:"autocreated_devices"
  12. CapAdd []string `json:"cap_add"
  13. CapDrop[]string `json:"cap_drop"
  14. ContainerPid int `json:"container_pid"// the pid for the process inside a container 
  15. ProcessConfig ProcessConfig `json:"process_config"// Describes the init process of the container. 
  16. ProcessLabel string `json:"process_label"
  17. MountLabel string `json:"mount_label"
  18. LxcConfig []string `json:"lxc_config"
  19. AppArmorProfile string `json:"apparmor_profile"

其中和進(jìn)程隔離相關(guān)的有 Resources 規(guī)定了 cpu 和 memory 的資源分配,可供 cgroups 將來調(diào)用。 CapAdd 和 CapDrop 這個和 linux Capability 相關(guān)來控制 root 的某些系統(tǒng)調(diào)用權(quán)限不會被容器內(nèi)的程序使用。ProcessLabel 為容器內(nèi)的進(jìn)程打上一個 Lable 這樣的話 seLinux 將來就可以通過這個 lable 來做權(quán)限控制。Apparomoprofile 指向 docker 默認(rèn)的 apparmor profile 路徑,一般為/etc/apparmor.d/docker,用來控制程序?qū)ξ募到y(tǒng)的訪問權(quán)限。

可以看到,docker 對容器的隔離策略并不是自己開發(fā)一套隔離機(jī)制而是把現(xiàn)有的能用的已有隔離機(jī)制全用上。甚至 AppArmor 和 seLinux 這兩個類似并且人家兩家還在相互競爭的機(jī)制也都一股腦不管三七二十一全加上,頗有拿來主義的風(fēng)采。這樣的話萬一惡意程序突破了一層防護(hù)還有另外一層擋著,而且這幾個隔離機(jī)制還相互保護(hù)要同時突破所有的防護(hù)才行。

而我們真正要在容器中執(zhí)行的程序在 ProcessConfig 這個結(jié)構(gòu)體中的 Entrypoint。由此可見所謂的容器就是一個穿著各種隔離外套的程序,用這些隔離外套保護(hù)這個程序可以活在自己的小天地里,不知有漢無論魏晉。
Exec

還是回到 run 里面看看究竟是怎么 run 的吧,看完了一系列的初始化和異常判斷后終于到了真正運行的代碼,只有一行,長得是這個樣子的:

 

  1. return namespaces.Exec(container, c.ProcessConfig.Stdin, c.ProcessConfig.Stdout, c.ProcessConfig.Stderr, c.ProcessConfig.Console, dataPath, args, func(container *libcontainer.Config, console, dataPath, init string, child *os.File, args []string) *exec.Cmd { 
  2. c.ProcessConfig.Path = d.initPath 
  3. c.ProcessConfig.Args = append([]string{ 
  4. DriverName, 
  5. "-console", console, 
  6. "-pipe""3"
  7. "-root", filepath.Join(d.root, c.ID), 
  8. "--"
  9. }, args...) 
  10.  
  11. // set this to nil so that when we set the clone flags anything else is reset 
  12. c.ProcessConfig.SysProcAttr = &syscall.SysProcAttr{ 
  13. Cloneflags: uintptr(namespaces.GetNamespaceFlags(container.Namespaces)), 
  14. c.ProcessConfig.ExtraFiles = []*os.File{child} 
  15.  
  16. c.ProcessConfig.Env = container.Env 
  17. c.ProcessConfig.Dir = container.RootFs 
  18.  
  19. return &c.ProcessConfig.Cmd 
  20. }, func() { 
  21. if startCallback != nil { 
  22. c.ContainerPid = c.ProcessConfig.Process.Pid 
  23. startCallback(&c.ProcessConfig, c.ContainerPid) 
  24. }) 

 

看到這里整個人都不好了,我覺得 docker 這個項目要是這樣下去會出問題的,就算你喜歡匿名函數(shù)也不要這么偏執(zhí)好么。我甚至懷疑 docker 在用什么黑科技來隱藏他的真實代碼了。于是我決定放棄這行代碼直接看 namespaces.Exec 去了。在\verdor\src\github.com\libcontainer\namespaces\exec.go里

 

  1. func Exec(container *libcontainer.Config, stdin io.Reader, stdout, stderr io.Writer, console, dataPath string, args []string, createCommand CreateCommand, startCallback func()) (int, error)  

不太確定一個函數(shù)8個參數(shù)真的好么,但是我更納悶的是在主項目里既然都有 pipe 這個結(jié)構(gòu)把 stdin,stdout,stderr 放在一起為啥到這里就要分開寫了,6個雖然也不少,但是比8個要好點。回過頭來說一下 namespace ,這又是另一種隔離機(jī)制。顧名思義,隔離的是名字空間,這要的話本來屬于全局可見的名字資源,如 pid,network,mountpoint 之類的資源虛擬出多份,每個 namespace 一份,每組進(jìn)程占用一個 namespace。這樣的話容器內(nèi)程序都看不到外部其他進(jìn)程,攻擊的難度自然也就加大了。

然后這里面最關(guān)鍵的執(zhí)行的一句倒是很簡單了。

 

  1. if err := command.Start(); err != nil { 
  2. child.Close() 
  3. return -1, err 

其中的 command 是系統(tǒng)調(diào)用類 exec.Cmd 的一個對象,而之前的關(guān)于程序的配置信息已經(jīng)在那個一行的執(zhí)行代碼里都整合進(jìn) command 里了,在這里只要 start 一下程序就跑起來了。然后我就疑惑了,這個函數(shù)不是 namespaces 包下的么,咋沒有 namespaces 設(shè)置的相關(guān)代碼呢。其實你仔細(xì)看那一行的執(zhí)行代碼可以發(fā)現(xiàn) namespaces 的設(shè)置也在里面了,換句話說這個 namespaces 包下的 exec 其實沒有做什么和 namespaces 相關(guān)的事情,只是 start 了一下。這種代碼邏輯結(jié)構(gòu)可是給讀代碼的人帶來了不小的困惑啊。
總結(jié)

這次讀代碼的起點是想搞懂容器是如何做隔離和保證安全的。從代碼來看 docker 并沒有另起爐灶新開發(fā)機(jī)制,而是將現(xiàn)有經(jīng)過考驗的隔離安全機(jī)制能用的全用上,包括 cgroups,capability,namespaces,apparmor 和 seLinux。這樣一套組合拳打出來的效果理論上看還是很好的,即使其中一個機(jī)制出了漏洞,但是要利用這個漏洞的方法很可能會被其他機(jī)制限制住,要找到一種同時繞過所有隔離機(jī)制的方法難度就要大多了。

但是從讀代碼的角度來看,docker 的代碼的質(zhì)量就讓人很難恭維了,即使 libcontainer 是一個獨立的部分,但本是同根生的名字都不一致,不知道之后會不會更混亂。而一些代碼風(fēng)格和邏輯上也實在讓人讀起來很費勁,代碼質(zhì)量要提高的地方還有很多。畢竟是開源的項目,即使功能很強大,但是大家如果發(fā)現(xiàn)代碼質(zhì)量有問題,恐怕也不大敢用在生產(chǎn)吧。

而至于 libcontainer 盡管從 docker 中獨立出去發(fā)展,但是可以看出和主項目還有一些沒有切分干凈的地方,而且 docker 主項目目前也沒有采用 libcontainer 中的 container 方式,只是在調(diào)用里面的一些機(jī)制方法,看樣子目前還處于一個逐步替換的過程中。libcontainer 和一個獨立完整的產(chǎn)品還有一段距離,諸位有興趣的也可以參與進(jìn)去,萬一這就是下一個偉大的項目呢?

原文出自:https://docker.cn/p/docker-libcontainer-reading

 

責(zé)任編輯:Ophira 來源: Docker中文社區(qū)
相關(guān)推薦

2017-01-23 12:40:45

設(shè)計演講報表數(shù)據(jù)

2011-06-30 22:23:21

打印機(jī)常見問題

2020-11-02 09:48:35

C++泄漏代碼

2016-01-07 12:40:02

機(jī)器學(xué)習(xí)權(quán)威定義

2021-04-02 06:18:27

Docker鏡像

2025-02-05 11:43:28

2024-09-24 10:36:29

2024-09-26 19:39:23

2011-06-28 10:41:50

DBA

2020-07-08 07:44:35

面試阿里加班

2017-03-22 15:38:28

代碼架構(gòu)Java

2021-12-27 10:08:16

Python編程語言

2020-10-24 13:50:59

Python編程語言

2023-09-04 09:12:10

設(shè)計業(yè)務(wù)耦合

2013-11-20 13:55:01

代碼提交優(yōu)秀

2024-09-26 10:41:31

2018-07-16 22:29:29

代碼迭代質(zhì)量

2016-06-23 14:19:59

DevOpsOpenStackIaaS

2012-08-28 09:21:59

Ajax查錯經(jīng)歷Web

2021-11-01 17:29:02

Windows系統(tǒng)Fork
點贊
收藏

51CTO技術(shù)棧公眾號