自己动手写docker——实现六个namespace隔离

  • Post author:
  • Post category:其他

自己动手写docker

<<自己动手写docker>>阅读实践笔记

docker理论基础这部分不再赘述,可以看我这篇文章一文带你全方位入门docker

此次实践使用的IDE为Goland,操作系统为mac,代码运行环境为Cent OS7,内核版本为3.10.0-957.21.3.el7.x86_64

开始前,需要修改Goland,Preferences–>Go–>OS改为linux,内核版本为。否则代码编写时一些linux only的系统调用会报错。

https://golang.org/pkg/syscall/#SysProcAttr

1. 实现六个namespace隔离

1.1. 实现UTS namespace隔离

通过syscall.CLONE_NEWUTS创建一个新的UTS namespace

package main

import (
	"log"
	"os"
	"os/exec"
	"syscall"
)

func main()  {
	cmd := exec.Command("sh")
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Cloneflags: syscall.CLONE_NEWUTS,
	}
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	if err := cmd.Run(); err != nil{
		log.Fatal(err)
	}
}

在centos上运行此代码go run 1.uts_ns.go进入一个交互式的环境

通过pstree -pl查询进程树

$ pstree -pl | grep go
           |-sshd(21453)---sshd(27698)---bash(27701)---go(16497)-+-1.uts_ns(16514)-+-sh(16517)-+-grep(16564)

这时去查询父进程和子进程的UTS namespace

sh-4.2# ls /proc/16514/ns/uts  -al
lrwxrwxrwx 1 root root 0 7月   6 13:41 /proc/16514/ns/uts -> uts:[4026531838]
sh-4.2# ls /proc/16517/ns/uts  -al
lrwxrwxrwx 1 root root 0 7月   6 13:41 /proc/16517/ns/uts -> uts:[4026532283]

可以看到不在同一个UTS namespace中,UTS namespace主要用来隔离主机名和域名,现在我们来修改主机名进行验证。

sh-4.2# hostname newuts
sh-4.2# hostname
newuts

再打开一个终端查看宿主机主机名可以验证主机名并没有被内部程序更改

[root@external_node ~]# hostname
external_node

1.2. 实现IPC namespace隔离

IPC namespace用来隔离System V IPC 和 POSIX message queues。通过syscall.CLONE_NEWIPC,创建一个新的UTS namespace

package main

import (
	"log"
	"os"
	"os/exec"
	"syscall"
)

func main()  {
	cmd := exec.Command("sh")
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC,
	}
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	if err := cmd.Run(); err != nil{
		log.Fatal(err)
	}
}

通过ipcmk创建ipc资源

[root@external_node docker_study]# ipcmk -Q
消息队列 id:0
[root@external_node docker_study]# ipcs -q

--------- 消息队列 -----------
键        msqid      拥有者  权限     已用字节数 消息      
0xc2bc06da 0          root       644        0            0           

运行我们的代码,在新的namespace中,看不到宿主机的消息。

sh-4.2# ipcs  -q

--------- 消息队列 -----------
键        msqid      拥有者  权限     已用字节数 消息      

1.3. 实现PID namespace隔离

PID namespace用来隔离进程的ID,通过syscall.CLONE_NEWPID来实现。

package main

import (
	"log"
	"os"
	"os/exec"
	"syscall"
)

func main()  {
	cmd := exec.Command("sh")
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID,
	}
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	if err := cmd.Run(); err != nil{
		log.Fatal(err)
	}
}

运行程序

[root@external_node docker_study]# go run 3.pid_ns.go
sh-4.2# pstree -pl
...
           │             └─sshd(27698)───bash(27701)───go(25623)─┬─3.pid_ns(25640)─┬─sh(25643)───pstree(25740)
...
sh-4.2# echo $$
1

当前的namespace下进程ID为1,在宿主机上为25640

1.4. 实现Mount namespace隔离

Mount namespace用来隔离各个进程看到的挂载点视图,不同的namespace看到的文件系统层次是不一样的,通过 syscall.CLONE_NEWNS来实现

package main

import (
	"log"
	"os"
	"os/exec"
	"syscall"
)

func main()  {
	cmd := exec.Command("sh")
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,
	}
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	if err := cmd.Run(); err != nil{
		log.Fatal(err)
	}
}

运行我们的代码

[root@external_node docker_study]# go run 4.mount_ns.go 
sh-4.2# ls /proc/
1      15     22     27438  3141  9976	       irq	     schedstat
10     15826  23     27442  32	  acpi	       kallsyms      scsi
100    15917  23243  27514  33	  buddyinfo    kcore	     self
11     16     23301  27520  41	  bus	       keys	     slabinfo
1103   16755  24     27537  43	  cgroups      key-users     softirqs
1175   16771  24820  27698  44	  cmdline      kmsg	     stat
1182   16899  25077  27701  45	  consoles     kpagecount    swaps
1183   17     2571   2879   46	  cpuinfo      kpageflags    sys
1265   18     2573   29103  5	  crypto       loadavg	     sysrq-trigger
1266   18411  2574   29108  59	  devices      locks	     sysvipc
12717  18734  2575   29125  666   diskstats    mdstat	     timer_list
13     18737  2577   2946   680   dma	       meminfo	     timer_stats
1362   18870  26168  2953   689   driver       misc	     tty
13875  19     26774  2962   696   execdomains  modules	     uptime
13880  194    27191  3	    699   fb	       mounts	     version
1389   2      27389  30     7	  filesystems  mtrr	     vmallocinfo
13893  20     27391  3032   8	  fs	       net	     vmstat
1391   21     27401  3038   9	  interrupts   pagetypeinfo  zoneinfo
13910  21157  27421  31     949   iomem        partitions
14     21453  27424  3139   9702  ioports      sched_debug

查看/proc的内容。proc是一个文件系统,提供额外的机制,可以通过内核和内核模块将信息发送给进程。mount()unmount()仅仅影响当前namespace内的文件系统,所以们现在将宿主机/proc挂在到当前namespace,再查看/proc就是当前namespace的内容了

sh-4.2# mount -t proc proc /proc
sh-4.2# ls /proc/
1	   devices	ioports     locks	  sched_debug	 sysvipc
4	   diskstats	irq	    mdstat	  schedstat	 timer_list
acpi	   dma		kallsyms    meminfo	  scsi		 timer_stats
buddyinfo  driver	kcore	    misc	  self		 tty
bus	   execdomains	keys	    modules	  slabinfo	 uptime
cgroups    fb		key-users   mounts	  softirqs	 version
cmdline    filesystems	kmsg	    mtrr	  stat		 vmallocinfo
consoles   fs		kpagecount  net		  swaps		 vmstat
cpuinfo    interrupts	kpageflags  pagetypeinfo  sys		 zoneinfo
crypto	   iomem	loadavg     partitions	  sysrq-trigger
sh-4.2# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 14:33 pts/0    00:00:00 sh
root         5     1  0 14:36 pts/0    00:00:00 ps -ef

1.5. 实现User namespace 隔离

User namespace 主要是隔离用户的用户组ID,一个进程的User ID和Group ID在User namespace内外可以是不同的。在宿主机创建一个普通用户,在别的User namespace可以映射为root用户。通过syscall.CLONE_NEWUSER来实现。

centos7需要先开启User namespace,然后重启。

grubby --args="user_namespace.enable=1" --update-kernel="$(grubby --default-kernel)"
reboot
echo 640 > /proc/sys/user/max_user_namespaces

代码如下

package main

import (
	"log"
	"os"
	"os/exec"
	"syscall"
)

func main()  {
	cmd := exec.Command("sh")
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWUSER,
		UidMappings: []syscall.SysProcIDMap{
			{
				ContainerID: 10000,
				HostID:      0,
				Size:        1,
			},
		},
		GidMappings: []syscall.SysProcIDMap{
			{
				ContainerID: 10000,
				HostID:      0,
				Size:        1,
			},
		},
	}

	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	if err := cmd.Run(); err != nil{
		log.Fatal(err)
	}
	os.Exit(-1)
}

运行代码

[root@external_node docker_study]# go run 5.user_ns.go 
sh-4.2$ id
uid=10000 gid=10000 组=10000

1.6. 实现Network namespace隔离

Network namespace用来隔离网络设备,IP地址端口等网络栈。Network namespace可以让每个容器拥有自己独立的网络设备。

package main

import (
	"log"
	"os"
	"os/exec"
	"syscall"
)

func main()  {
	cmd := exec.Command("sh")
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWUSER | syscall.CLONE_NEWNET,
	}

	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	if err := cmd.Run(); err != nil{
		log.Fatal(err)
	}
	os.Exit(-1)
}

运行代码

[root@external_node docker_study]# go run 6.network_ns.go 
sh-4.2$ ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
sh-4.2$ ifconfig

宿主机的网络设备,在当前namespace中都不可见。


版权声明:本文为DoloresOOO原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。