Contents

Linux Namespace 机制

Linux Namespace 提供了一种内核级别隔离系统资源的方法,通过将系统的全局资源放在不同的 Namespace 中,来实现资源隔离的目的。不同 Namespace 的程序,可以享有一份独立的系统资源。目前Linux中提供了六类系统资源的隔离机制

分类对应的宏定义相关内核版本
Mount:隔离文件系统挂载点CLONE_NEWNSLinux 2.4.19
UTS: 隔离主机名和域名信息CLONE_NEWNSLinux 2.6.19
IPC: 隔离进程间通信CLONE_NEWIPCLinux 2.6.19
PID: 隔离进程的IDCLONE_NEWPIDLinux 2.6.24
Network: 隔离网络资源CLONE_NEWNET始于Linux 2.6.24 完成于 Linux 2.6.29
User: 隔离用户和用户组的IDCLONE_NEWUSER始于 Linux 2.6.23 完成于 Linux 3.8

namespace 的主要作用:封装抽象,限制,隔离,使命名空间内的进程看起来拥有他们自己的全局资源

Network Namespace

Network namespaces 隔离了与网络相关的系统资源(这里罗列一些):

  • network devices - 网络设备
  • IPv4 and IPv6 protocol stacks - IPv4、IPv6 的协议栈
  • IP routing tables - IP 路由表
  • firewall rules - 防火墙规则
  • /proc/net (即 /proc/PID/net)
  • /sys/class/net
  • /proc/sys/net 目录下的文件
  • 端口、socket
  • UNIX domain abstract socket namespace
[root@lain1 ns]# cd /proc/$$/ns	# $$代表当前shell进程
[root@lain1 ns]# ll	# 格式为:namespace类型:[inode number]

创建网络隔离

首先我们可以创建一个命名为 test_ns 的 network namespace

[root@lain1 ns]# ip netns add test_ns
[root@lain1 ns]# ip netns list
test_ns

当 ip 命令工具创建一个 network namespace 时,会默认创建一个回环设备(loopback interface:lo),并在 /var/run/netns 目录下绑定一个挂载点,这就保证了就算 network namespace 中没有进程在运行也不会被释放,也给系统管理员对新创建的 network namespace 进行配置提供了充足的时间

通过 ip netns exec 命令可以在新创建的 network namespace 下运行网络管理命令

[root@lain1 ns]# ip netns exec test_ns ip link list
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

上面的命令为我们展示了新建的 namespace 下可见的网络链接,可以看到状态是 DOWN,需要再通过命令去启动。可以看到,此时执行 ping 命令是无效的

[root@lain1 ns]# ip netns exec test_ns ping 127.0.0.1
connect: Network is unreachable

启动命令如下,可以看到启动后再测试就可以 ping 通

[root@lain1 ns]# ip netns exec test_ns ip link set lo up
[root@lain1 ns]# ip netns exec test_ns ip link list
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
[root@lain1 ns]# ip netns exec test_ns ping 127.0.0.1
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.031 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.022 ms
64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.023 ms

这样只是启动了本地的回环,要实现与外部 namespace 进行通信还需要再建一个网络设备(veth)对,在宿主机执行如下命令

[root@lain1 ns]# ip link add veth0 type veth peer name veth1
[root@lain1 ns]# ip a
252: veth1@veth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 32:04:e1:55:5e:7b brd ff:ff:ff:ff:ff:ff
253: veth0@veth1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 3e:8b:dc:8a:c1:90 brd ff:ff:ff:ff:ff:ff

在宿主机上把 veth0 这一端分配到 test_ns 这个 namespace 上,把 veth1 这一端分配到 test_ns2 这个 namespace 上

[root@lain1 ns]# ip link set veth0 netns test_ns && ip link set veth1 netns test_ns2
[root@lain1 ns]# ip netns exec test_ns ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
253: veth0@if252: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 3e:8b:dc:8a:c1:90 brd ff:ff:ff:ff:ff:ff link-netnsid 1

分别给 ns 的网络设备分配 ip

[root@lain1 ns]# ip netns exec test_ns ip addr add local 10.1.1.1/24 dev veth0
[root@lain1 ns]# ip netns exec test_ns2 ip addr add local 10.1.1.2/24 dev veth1

分别启动 veth 设备,此时两个 ns 就可以互通了

[root@lain1 ns]# ip netns exec test_ns ip link set veth0 up
[root@lain1 ns]# ip netns exec test_ns2 ip link set veth1 up
[root@lain1 ns]# ip netns exec test_ns ping 10.1.1.2
PING 10.1.1.2 (10.1.1.2) 56(84) bytes of data.
64 bytes from 10.1.1.2: icmp_seq=1 ttl=64 time=0.051 ms
64 bytes from 10.1.1.2: icmp_seq=2 ttl=64 time=0.039 ms

删除 net namespace

[root@lain1 ns]# ip netns delete test_ns

UTS Namespace

UTS namespace 提供了主机名和域名的隔离,这样每一个容器就可以拥有独立的主机名和域名,在网络上可以被视为一个独立的节点而非宿主机上的一个进程

使用 Linux(centos7)集成工具 unshare 命令,可以创建不同的 namespace

[root@lain1 ~]# unshare --fork --uts /bin/bash # 启动主机名隔离
[root@lain1 ~]# hostname test
[root@lain1 ~]# hostname
test
[root@lain1 ~]# exit
[root@lain1 ~]# hostname
lain1

Mount Namespace

Mount namespace 通过隔离文件系统挂载点对隔离文件系统提供支持。隔离后,不同的 mount namespace 中的文件结构发生变化也互不影响。你可以通过 /proc/[pid]/mounts 查看到所有挂载在当前 namesapce 中的文件系统,还可以通过 /proc/[pid]/mountstats 看到 mount namespace 中文件设备的统计信息,包括挂载的文件名称,文件系统类型,挂载位置等等

进程在创建 mount namespace 的时候,会把当前结构复制给新的 namespace。 新的 namespace 中的所有 mount 操作都影响自身的文件系统,而对外界不会产生任何影响。这样做就严格地实现了隔离

[root@lain1 ~]# mkdir /mnt/mytemp						# 创建一个文件夹
[root@lain1 ~]# unshare --fork --uts --mount /bin/bash	# 同时实现主机名隔离和挂载隔离
[root@lain1 ~]# mount -t tmpfs myfs /mnt/mytemp			# 执行挂载
[root@lain1 ~]# df -h
myfs            3.9G     0  3.9G   0% /mnt/mytemp

User Namespace

User namespace 主要隔离了安全相关的标识符和属性,包括用户ID、用户组ID、root目录等。通俗点就是:一个普通用户的进程通过 clone() 创建新的进程在新 user namespace 中可以拥有不同的用户和用户组。这意味着一个进程在容器外属于一个没有特殊权限的普通用户,但是它创建的容器进程却属于拥有所有权限的超级用户,这个技术为容器提供了极大的自由

安装一些库

[root@lain1 ~]# curl https://forensics.cert.org/cert-forensics-tools-release-el7.rpm -o cert-forensics-tools-release-el7.rpm 
[root@lain1 ~]# rpm -Uvh cert-forensics-tools-release*rpm
[root@lain1 ~]# yum --enablerepo=forensics install -y musl-libc-static

下载 alpine,进入 bin 目录执行命令进入一个新的 sh 终端,好比进入容器进行隔离

[root@lain1 ~]# ./busybox sh

创建用户隔离

默认情况下,需要修改 max_user_namespaces 文件的值,默认是0

[root@lain1 ~]# echo 65535 > /proc/sys/user/max_user_namespaces

隔离创建新的用户 namespace,隔离所对应的进程是 busybox sh

[txl@lain1 ~]$ unshare --fork --user /home/txl/alpine/bin/busybox sh

/home/txl $ id			# 容器内默认的uid是65534
uid=65534 gid=65534 groups=65534

/home/txl $ echo $$		# 当前进程ID(还没有做进程隔离,所以显示的是宿主机的PID)
31920

默认会映射 /proc/sys/kernel/overflowuid/proc/sys/kernel/overflowgid

为 busybox 设置 capability 特权

linux 内核2.2之后引入了 capabilities 机制,来对 root 权限进行更加细粒度的划分。如果进程不是特权进程,而且也没有 root 的有效 id,系统就会去检查进程的 capabilities,来确认该进程是否有执行特权操作的的权限

[txl@lain1 ~]$ sudo setcap cap_setgid,cap_setuid+ep /home/txl/alpine/bin/busybox 
[txl@lain1 ~]$ getcap /home/txl/alpine/bin/busybox # 查看
/home/txl/alpine/bin/busybox = cap_setgid,cap_setuid+ep

[txl@lain1 ~]$ sudo setcap cap_setgid,cap_setuid-ep /home/txl/alpine/bin/busybox # 取消

给容器映射用户,方法是添加映射信息到 /proc/$$/uid_map/proc/$$/gid_map

# 父 namespace 中的 1000~1256 映射到新 user namespace 中的 0~256
[root@lain1 ~]# echo '0 1000 256' > /proc/31920/uid_map
[root@lain1 ~]# echo '0 1000 256' > /proc/31920/gid_map

此时容器内会立刻产生变化

/home/txl # id
uid=0(root) gid=0(root) groups=0(root),65534

PID Namespace

PID namespace 主要是用于隔离进程号。即,在不同的 PID namespace 中可以包含相同的进程号,每个 PID namespace 中的第一个进程“PID 1“,都会像 init 进程一样拥有特权

PID namespace 中的 1 号进程是所有孤立进程的父进程,如果这个进程被终止,内核将调用 SIGKILL 发出终止此 namespace 中的所有进程的信号,这部分内容与 Kubernetes 中应用的优雅关闭/平滑升级等都有一定的联系。从 Linux v3.4 内核版本开始,如果在一个 PID namespace 中发生 reboot() 的系统调用,则 PID namespace 中的 init 进程会立即退出。这算是一个比较特殊的技巧,可用于处理高负载机器上容器退出的问题

[root@lain1 ~]# unshare --fork --pid --mount /home/txl/alpine/bin/busybox sh
~ # mount -t proc proc /proc
~ # ps
  PID TTY          TIME CMD
    1 pts/0    00:00:00 busybox
    4 pts/0    00:00:00 ps

Go 实现 Namespace 隔离

从 docker 克隆镜像文件到本地

[root@lain1 ~]# docker run -d alpine:3.12 top -b
[root@lain1 ~]# docker export -o alpine.tar <container id>
[root@lain1 ~]# mkdir alpine && tar xf alpine.tar -C ./alpine

根命令定义

var rootCmd = &cobra.Command{
	Use:   "linux-namespace",
	Short: "",
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("linux namespace dev")
	},
}

func Execute() {
	err := rootCmd.Execute()
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

func init() {
	rootCmd.AddCommand(runCommand, execCommand)
}

run 参数定义

const self = "/proc/self/exe" //在Linux中代表当前执行的程序
const alpine = "/home/txl/alpine"

var runCommand = &cobra.Command{
	Use: "run",
	Run: func(cmd *cobra.Command, args []string) {
		runCmd := exec.Command(self, "exec", "/bin/busybox", "sh")	// 产生一个新进程调用自身的exec参数
        // 主机名隔离 用户隔离 挂载隔离 进程隔离
		runCmd.SysProcAttr = &syscall.SysProcAttr{
			Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWUSER | syscall.CLONE_NEWNS | syscall.CLONE_NEWPID,
			UidMappings: []syscall.SysProcIDMap{	// 设置uid_map
				{
					ContainerID: 0,
					HostID:      os.Getuid(),
					Size:        1,
				},
			},
			GidMappings: []syscall.SysProcIDMap{	// 设置gid_map
				{
					ContainerID: 0,
					HostID:      os.Getgid(),
					Size:        1,
				},
			},
		}
		runCmd.Stdin = os.Stdin
		runCmd.Stdout = os.Stdout
		runCmd.Stderr = os.Stderr
		if err := runCmd.Start(); err != nil {
			log.Fatalln(err)
		}
		runCmd.Wait()
	},
}

exec 参数定义

const env = "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:mytest"
var execCommand = &cobra.Command{
	Use: "exec",
	Run: func(cmd *cobra.Command, args []string) {
		if len(args) == 0 {
			log.Fatalln("error args")
		}
		var runArgs []string
		if len(args) > 1 {
			runArgs = args[1:]	// sh
		}
        
        if err := syscall.Chroot(alpine); err != nil {	// 设置在指定的根目录下运行
			log.Fatalln(err)
		}
		if err := os.Chdir("/"); err != nil {	// 替换当前的工作目录
			log.Fatalln(err)
		}
        if err := syscall.Mount("proc", "/proc", "proc", 0, ""); err != nil {	// 挂载proc
			log.Fatalln(err)
		}
        
		runCmd := exec.Command(args[0], runArgs...)	// /bin/busybox sh
		runCmd.Stdin = os.Stdin
		runCmd.Stdout = os.Stdout
		runCmd.Stderr = os.Stderr
        runCmd.Env = []string{env}	// 设置容器环境变量
		if err := runCmd.Start(); err != nil {
			log.Fatalln(err)
		}
		runCmd.Wait()
	},
}

参考:浅谈 Linux Namespace