Linux namespace概述(二)

理解pid namespace

PID namespace表示隔离一个具有独立PID的运行环境。在每一个pid namespace中,进程的pid都从1开始,且和其他pid namespace中的PID互不影响。这意味着,不同pid namespace中可以有相同的PID值。

因为PID namespace中的PID是独立的,每一个PID namespace都允许一些特殊的操作:允许pid namespace挂起、迁移以及恢复,就像虚拟机一样。

在介绍PID namespace之前,有必要回顾一下创建其他类型namespace时的进程关系。

# 在root namespace中
$ echo $$
12314
$ sudo unshare -u /bin/bash
[ns1]$ pstree -p | grep grep
   |   `-sshd(12313)---bash(12314)---sudo(14930)---bash(14931)-+-grep(14942)

其中sudo在root namespace中,其子进程bash(14931)在新创建的uts namespace ns1中。从上面输出的结果可知,创建其他类型namespace时,unshare进程会在创建新的namespace后被该namespace中的第一个进程给替换掉。如果忘记了,请回到前文uts namespace复习复习。

了解了创建其他类型namespace进程关系的基础后,再来对比介绍pid namespace。

创建新的pid namespace的方式:

# unshare --pid --fork [--mount-proc] <CMD>
# --pid或-p表示创建pid namespace
# --fork或-f表示创建pid namespace时,不是直接
#   替换unshare进程,而是fork unshare进程,
#   并使用CMD替换fork出来的子进程
# --mount-proc表示创建pid namespace时重新挂载procfs
sudo unshare -p -f -m -u --mount-proc /bin/bash

--fork以及--mount-proc选项稍后再解释。先看看创建pid namespace后的进程关系:

$ sudo unshare -p -f -m -u --mount-proc /bin/bash
root@longshuai-vm:/home/longshuai# hostname ns1
root@longshuai-vm:/home/longshuai# exec bash
root@ns1:/home/longshuai# pstree -p | grep bash
bash(1)-+-grep(11)    # pid namespace中,第一个进程bash其PID=1
                      # 该namespace中没有其他进程

# 在第二个shell窗口会话中执行
$ pstree -p | grep sudo
 |  `-sshd(12313)---bash(12314)---sudo(15070)---unshare(15071)---bash(15072)

注意上面输出的进程关系,和之前创建普通的namespace的进程关系不同,创建PID namespace时,sudo的子进程unshare进程保留了,这就是命令行中使用--fork的效果:在unshare中创建出pid namespace后,它将fork出它的子进程加入到新的pid namespace中,并在该子进程中exec加载指定的/bin/bash进程作为该pid namespace中的第一个进程。

所以使用--fork后导致的结果是:unshare进程被保留,且保留在原来的pid namespace中,而不是加入新的pid namespace中(在man pid_namespaces中明确指出了创建pid namespace的unshare或setns进程不会也不能进入新的pid namespace)。

# unshare进程和bash(12314)、sudo(15070)都在同一个pid namespace
$ pstree -p | grep sudo
 |  `-sshd(12313)---bash(12314)---sudo(15070)---unshare(15071)---bash(15072)

$ sudo ls -l /proc/12314/ns/pid
lrwxrwxrwx ... /proc/12314/ns/pid -> 'pid:[4026531836]'
$ sudo ls -l /proc/15070/ns/pid
lrwxrwxrwx ... /proc/15070/ns/pid -> 'pid:[4026531836]'
$ sudo ls -l /proc/15071/ns/pid
lrwxrwxrwx ... /proc/15071/ns/pid -> 'pid:[4026531836]'

# 但unshare的子进程bash在新的pid namespace中
$ sudo ls -l /proc/15072/ns/pid
lrwxrwxrwx ... /proc/15072/ns/pid -> 'pid:[4026532590]'

在新创建的pid namespace中使用ps命令查看进程信息:

BASH

root@ns1:~# ps j
   PPID   PID  PGID  SID TTY    TPGID STAT  UID   TIME COMMAND
      0     1     1    0 pts/0     21 S       0   0:00 bash
      1    21    21    0 pts/0     21 R+      0   0:00 ps j

关注输出结果中的两点:

1.pid namespace中的第一个进程bash,其PID=1

2.PID=1的进程的PPID=0,这和root namespace中pid=1的init进程的PPID=0是一样的

BASH

$ ps -elf
F S UID   PID    PPID  ... CMD
4 S root    1       0  ... /sbin/init splash
1 S root    2       0  ... [kthreadd]
...

在pid namespace中,PID=1的进程作为该pid namespace环境的【init】进程,当该pid namespace中的某个进程A的父进程退出了,进程A将称为孤儿进程,孤儿进程将被该pid namespace中PID=1的进程收养。

另外,观察旧namespace以及新pid namespace中的进程:

BASH

# 原来的namespace
$ pstree -p | grep sudo
 |  `-sshd(12313)---bash(12314)---sudo(15070)---unshare(15071)---bash(15072)
 
# 新创建的pid namespace
root@ns1:/home/longshuai# pstree -p | grep bash
bash(1)-+-grep(11)

其实unshare的子进程bash(15072)和pid namespace中的bash(1)是同一个进程。查看它们的pid namespace的inode可知:

# 原来的namespace中执行
$ sudo readlink /proc/16158/ns/pid
pid:[4026532593]

# 新的pid namespace中执行
root@longshuai-vm:/home/longshuai# readlink /proc/$$/ns/pid
pid:[4026532593]

所以可得知一个结论:当在namespace ns1中创建一个pid namespace ns2,祖先ns1中可查看ns2中的所有进程,但ns2中无法查看祖先ns1中的进程。

另外需要注意的是,--fork并非一定是结合--pid创建pid namespace使用的,它也可以直接使用或结合其他类型的namespace选项使用。例如:

$ sudo unshare --fork sleep 3

这仅仅表示在一个子进程中执行sleep程序,这里的unshare并没有创建出namespace,所以上面的命令和在子shell中执行sleep命令没什么区别,例如(sleep 3)

嵌套pid namespace

其实,对于pid namespace来说,是存在嵌套关系的。所有的子孙pid namespace中的进程信息都会保存在父级以及祖先级namespace中,只不过在不同嵌套层级中,同一个进程对应的PID不同。

例如,创建嵌套了2层的pid namespace:

BASH

# 当前namespace为ns0

# 创建pid namespace ns1
$ sudo unshare -mupf --mount-proc /bin/bash

# 在ns1中创建pid namespace ns2
root@longshuai-vm:/home/longshuai# unshare -mupf --mount-proc /bin/bash

# 在ns2中创建pid namespace ns3
root@longshuai-vm:/home/longshuai# unshare -mupf --mount-proc /bin/bash

现在ns0有3个子孙级的pid namespace:ns1、ns2和ns3。

在ns0中查看进程关系:

BASH

$ pstree -lp | grep -oE 'sudo.*'
sudo(16510)---unshare(16511)---bash(16513)---unshare(16531)---bash(16532)---unshare(16539)---bash(16540)

其中每一对unshare---bash代表一个pid namespace(但注意,unshare不在其子进程bash所在的pid namespace中)。

BASH

unshare(16511)---bash(16513)
unshare(16531)---bash(16532)
unshare(16539)---bash(16540)

这三个bash进程在各自的pid namespace中的PID=1。但从输出结果可知,在ns0中,这三个pid namespace中的PID分别为:

BASH

bash(16513)
bash(16532)
bash(16540)

PID=16513对应ns1中的bash,PID=16532对应ns2中的bash,PID=16540对应ns3中的bash。

/proc/<PID>/status中的NSPID字段记录了当前进程<PID>在各父级pid namespace中对应的PID值。

例如,在ns0中查看ns3中的bash(pid=16540)进程:

BASH

$ grep 'NSpid' /proc/16540/status
NSpid:  16540    25      9       1
#       当前ns   ns1     ns2     ns3

这表示pid=16540这个进程对应ns1中的pid=25,对应ns2中的pid=9,对应ns3中的pid=1。

同样地,可以进入到ns1中(bash pid=16513)去查看进程pid=25的进程对应关系:

BASH

# nsenter命令可进入一个已存在的namespace,
#   -t PID指定进入哪一个目标(target)namespace,
#      PID可以是属于目标namespace中的任何一个进程
#   -F 表示不fork nsenter进程,而是直接使用/bin/bash替换nsenter进程
#      所以,nsenter允许不fork,但unshare创建pid namespace时必须fork
# 就像登录系统一样,比如ssh登录时会启动bash(或其他shell),
# 这可以看作是进入root namespace并启动一个bash环境
# nsenter用法很简单,可查看nsenter --help或man nsenter
$ sudo nsenter -m -u -p -F -t 16513 /bin/bash

# 在ns1中查看进程关系
# bash(1)是当前ns1中的第一个进程
# bash(17)是ns2中的第一个进程
# bash(25)是ns3中的第一个进程
root@longshuai-vm:/# pstree -pl | grep unshare
bash(1)---unshare(16)---bash(17)---unshare(24)---bash(25)

# 在ns1中查看ns3中的bash进程在各父级pid namespace中的对应关系
# ns3 bash在ns1中的进程PID=25
#         在ns2中的进程PID=9
#         在ns3中的进程PID=1
root@longshuai-vm:/# grep 'NSpid' /proc/25/status
NSpid:  25      9       1

pid namespace和procfs(/proc)

/proc目录是内核对外暴露的可供用户查看或修改的内核中所记录的信息,包括内核自身的部分信息以及每个进程的信息。比如对于pid=N的进程来说,它的信息保存在/proc/<N>目录下。

/proc是一个挂载点,是伪文件系统procfs的挂载点,文件系统类型是proc:

BASH

$ mount | grep proc
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)

在操作系统启动的过程中,会挂载procfs到/proc目录,它存在于root namespace中。

但是,创建新的pid namespace时不会自动重新挂载procfs,而是直接拷贝父级namespace的挂载点信息。这使得在新的pid namespace中仍然保留了父级namespace的/proc目录,也就是在新创建的这个pid namespace中仍然保留了父级的进程信息。

BASH

# 在root namespace中查看pid namespace的inode
$ readlink /proc/self/ns/pid
pid:[4026531836]

# 创建pid namespace,注意没有加上--mount-proc选项
$ sudo unshare -m -u -p -f -w"/" /bin/bash

# 进入pid namespace后,查看PID=1的进程pid inode
# 在pid namespace中,pid=1的进程是bash,查看结果
# 本该是新的pid namespace中的pid inode值,但实际
# 结果是root namespace中pid=1的init进程的pid inode
root@longshuai-vm:/# readlink /proc/1/ns/pid  
pid:[4026531836]

# 通过ps查看pid=1的进程,发现是systemd(init),而不是bash
root@longshuai-vm:/# ps -p 1 -o pid,ppid,comm
    PID    PPID COMMAND
      1       0 systemd

之所以有上述问题,其原因是在pid namespace中保留了root namespace中的/proc目录,而不是属于pid namespace自己的/proc。

但用户创建pid namespace时希望的是有完全独立的进程运行环境。这时,需要在pid namespace中重新挂载procfs,或者在创建pid namespace时指定--mount-proc选项。

BASH

# 重新挂载procfs
root@longshuai-vm:/# mount -t proc proc /proc

# 重新挂载procfs后,/proc目录保存的是当前
# pid namespace中的进程信息
root@longshuai-vm:/# ps -p 1 -o pid,ppid,comm
    PID    PPID COMMAND
      1       0 bash
      
root@longshuai-vm:/# readlink /proc/1/ns/pid  
pid:[4026532591]

# 或者直接在创建pid namespace时加上--mount-proc选项
# sudo unshare -p -f -m -u --mount-proc /bin/bash

pid namespace的信号问题

pid=1的进程是每一个pid namespace(无论是root namespace还是用户自己创建的pid namespace)的核心进程,它不仅负责收养其所在pid namespace中的孤儿进程,还影响整个pid namespace。

当pid namespace中pid=1的进程退出或终止,内核默认会发送SIGKILL信号给该pid namespace中的所有进程以便杀掉它们(如果该pid namespace中有子孙namespace,也会直接被杀)。

在创建pid namespace时可以通过--kill-child选项指定pid=1的进程终止后内核要发送给pid namespace中进程的信号,其默认信号便是SIGKILL。

BASH

# 例如指定发送SIGHUP信号
sudo unshare -p -f -m -u --mount-proc --kill-child=SIGHUP /bin/bash

在pid namespace内部,只能向pid=1的进程发送那些在pid=1的进程中注册了信号处理程序的信号。

例如,如果pid namespace中/bin/bash作为pid=1的进程,那么在此环境中,只能向该bash进程发送设置了trap的信号,其他信号都被直接忽略,这样可以避免无法信号导致pid namespace被自己内部的进程终止。

但对于pid=1的/bin/bash做测试,发现SIGHUP信号总是有效。

BASH

$ sudo unshare -p -f -m -u --mount-proc -w'/' /bin/bash

# 设置SIGINT信号的信号处理程序,现在bash进程只接收SIGINT信号
root@longshuai-vm:/# trap 'echo int' INT

# 发送INT信号有效
root@longshuai-vm:/# /bin/kill -INT 1
int

# 发送其他信号无效,即便是STOP和KILL信号
root@longshuai-vm:/# /bin/kill -QUIT 1
root@longshuai-vm:/# /bin/kill -STOP 1
root@longshuai-vm:/# /bin/kill -KILL 1

# 发送HUP信号也有效,即使没有设置HUP信号处理程序
root@longshuai-vm:/# /bin/kill -HUP 1
$  # 已退出pid namespace

在pid namespace外部,只有其祖先级namespace可发送信号给该pid namespace中的进程,因为其他namespace中看不到该pid namespace中的进程信息。但是发送信号时要找准pid namespace中的进程在祖先namespace中的PID值。

例如:

BASH

# 在root namespace中,创建pid namespace ns1,并睡眠30秒
$ sudo unshare -p -f -m -u --mount-proc -w'/' /bin/bash
root@longshuai-vm:/# sleep 300

# 打开第二个会话窗口,在root namespace中杀掉ns1中的sleep进程
$ pstree -lp | grep -o "sleep.*"
sleep(24685)
$ sudo kill 24685

理解network namespace

network namespace用来隔离网络环境,在network namespace中,网络设备、端口、套接字、网络协议栈、路由表、防火墙规则等都是独立的

因network namespace中具有独立的网络协议栈,因此每个network namespace中都有一个lo接口,但lo接口默认未启动,需要手动启动起来。

BASH

# -n或--net选项用于创建network namespace
$ sudo unshare -n /bin/bash

# 默认未启动lo
root@longshuai-vm:/home/longshuai# 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

# 将之启动
root@longshuai-vm:/home/longshuai# ip link set lo up
root@longshuai-vm:/home/longshuai# 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

让某个network namespace和root network namespace或其他network namespace之间保持通信是一个非常常见的需求,这一般通过veth虚拟设备实现。veth类型的虚拟设备由一对虚拟的eth网卡设备组成,像管道一样,一端写入的数据总会从另一端流出,从一端读取的数据一定来自另一端。

用户可以将veth的其中一端放在某个network namespace中,另一端保留在root network namespace中。这样就可以让用户创建的network namespace和宿主机通信。

例如:

BASH

##### 在第一个shell窗口
# 创建network namespace ns1
$ sudo unshare -n /bin/bash

# 查看该network namespace的进程号
root@longshuai-vm:/home/longshuai# echo $$
53091


##### 在第二个shell窗口
# 创建veth设备
$ sudo ip link add veth0 type veth peer name veth1

# 创建之后,就有了一对虚拟的eth设备
$ ip a | grep veth
3: veth1@veth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
4: veth0@veth1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000

# veth0准备留在root network namespace中
# 为veth0设置IP地址并启动
$ sudo ip a add dev veth0 192.168.10.10/24
$ sudo ip link set veth0 up
$ ip a s veth0
4: veth0@veth1: <NO-CARRIER,BROADCAST,MULTICAST,UP,M-DOWN> mtu 1500 qdisc noqueue state LOWERLAYERDOWN group default qlen 1000
    link/ether 36:30:54:ba:d3:aa brd ff:ff:ff:ff:ff:ff
    inet 192.168.10.10/24 scope global veth0
       valid_lft forever preferred_lft forever

# 最后,将veth1移动到刚才新建的network namespace ns1中
# ip link set xxx netns [ PID|NETNS_NAME ]
$ sudo ip link set veth1 netns 53091

# 注:
# 并不是所有的网络设备都能在network namespace之间移动,
# ethtool -k <interface>输出的结果中,netns-local为on的表示不能移动
$ ethtool -k lo | grep netns
netns-local: on [fixed]
$ ethtool -k ens32 | grep netns      
netns-local: off [fixed]
$ ethtool -k veth1 | grep netns                         
netns-local: off [fixed]

##### 在ns1中启动veth1并设置IP地址
root@longshuai-vm:/home/longshuai# ip link set dev veth1 up

root@longshuai-vm:/home/longshuai# ip a add dev veth1 192.168.10.20/24

root@longshuai-vm:/home/longshuai# ip a s veth1
3: veth1@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 22:a8:4b:5b:55:4d brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 192.168.10.20/24 scope global veth1
       valid_lft forever preferred_lft forever
    inet6 fe80::20a8:4bff:fe5b:554d/64 scope link 
       valid_lft forever preferred_lft forever 

# 互ping,测试两端是否能通信,例如在ns1测试
root@longshuai-vm:/home/longshuai# ping 192.168.10.10
PING 192.168.10.10 (192.168.10.10) 56(84) bytes of data.
64 bytes from 192.168.10.10: icmp_seq=1 ttl=64 time=0.076 ms
64 bytes from 192.168.10.10: icmp_seq=2 ttl=64 time=0.189 ms

但现在ns1还不能和公网通信。解决这个问题也很简单,在root network namespace中开启转发并设置SNAT,在ns1中添加默认路由即可。

BASH

##### 在root network namespace中执行
$ sudo sysctl -w net.ipv4.ip_forward=1
$ sudo iptables -t nat -A POSTROUTING -o ens32 -j MASQUERADE

##### 在network namespace ns1中执行
$ route add default gw 192.168.10.10
$ ping www.baidu.com
PING www.baidu.com (36.152.44.95) 56(84) bytes of data.
64 bytes from 36.152.44.95: icmp_seq=1 ttl=127 time=53.4 ms
64 bytes from 36.152.44.95: icmp_seq=2 ttl=127 time=51.0 ms

持久化的network namespace

正常情况下,当namespace中的所有进程都退出后,namespace也会随之销毁。但有时候需要让namespace即使没有进程在其中运行也依然有效,即namespace的持久化。例如,创建了network namespace后,想要让network namespace中的网络配置一直生效。

实际上,无论是network namespace还是其他类型的namespace,当通过mount bind为某个namespace的namespace文件(/proc/$$/ns/xxx)进行了bind挂载,这个namespace将成为持久化的namespace,即使namespace中的第一个进程或所有进程都退出了,namespace也不会立即销毁,之后还可以通过nsenter重新进入该namespace

其实方式很简单,直接在unshare创建namespace时的对应长选项上指定一个已存在的文件即可(注:不允许在短选项上指定文件名)。

unshare: 
 -m, --mount[=<file>]   unshare mounts namespace
 -u, --uts[=<file>]     unshare UTS namespace (hostname etc)
 -i, --ipc[=<file>]     unshare System V IPC namespace
 -n, --net[=<file>]     unshare network namespace
 -p, --pid[=<file>]     unshare pid namespace
 -U, --user[=<file>]    unshare user namespace
 -C, --cgroup[=<file>]  unshare cgroup namespace

 

例如,想要让network namespace持久化:

# 对于network namespace持久化,通常指定/var/run/netns/NAME作为持久化文件
$ sudo mkdir -p /var/run/netns
$ sudo touch /var/run/netns/ns1

# 也可以不在选项--net上指定文件,而是在network namespace内部
# 将/var/run/netns/ns1通过mount bind挂载到/proc/$$/ns/net上
$ sudo unshare --net=/var/run/netns/ns1 /bin/bash

# 现在/var/run/netns/ns1和/proc/$$/ns/net是同一个network namespace
root@longshuai-vm:/home/longshuai# ls -i /var/run/netns/ns1 
4026532587 /var/run/netns/ns1

root@longshuai-vm:/home/longshuai# readlink /proc/$$/ns/net
net:[4026532587]

# 退出ns1
root@longshuai-vm:/home/longshuai# exit
exit

# /var/run/netns/ns1仍然指向net inode
$ ls -i /var/run/netns/ns1 
4026532587 /var/run/netns/ns1

# nsenter重新进入ns1
$ sudo nsenter --net=/var/run/netns/ns1 /bin/bash
root@longshuai-vm:/home/longshuai# hostname -I
192.168.10.20

ip netns

ip netns命令用于管理network namespace。

ip netns将network namespace与/var/run/netns/NAME相关联,将/var/run/netns下的每一个NAME作为其所管理的每一个network namespace的名称。

ip netns将/etc/netns/NAME/作为对应network namespace的全局网络配置文件的目录,查找它之后才会查找/etc/目录。

例如,如果想要为netns名为ns1的network namespace单独设置DNS,可创建/etc/netns/ns1/resolv.conf,并将DNS相关配置写入该文件,当该文件不存在时才查找/etc/resolv.conf。

ip netns创建network namespace时,同时会创建mount namespace,以便将网络相关配置文件/etc/netns/NAME/xxx挂载到对应的/etc/xxx。

ip netns add NAME
创建名为NAME的network namespace,同时会关联/var/run/netns/NAME文件,如果文件不存在,则会自动创建

$ sudo ip netns add ns2
$ ls -i /var/run/netns/ns2
4026532759 /var/run/netns/ns2

ip netns list
列出/var/run/netns下的所有network namespace

# 带有id的,表示正在运行(即有进程尚未退出)的network namespace以及它的ID号
# ID会自动分配,从0开始,后面通过ip netns命令也可以自己设置ID号
$ ip netns list
ns2
ns1 (id: 0)

ip netns attach NAME PID
将PID对应的network namespace关联到/var/run/netns/NAME(不存在时会自动创建),使得该network namespace就像是被ip netns创建一样,之后它将受ip netns管理

# 在第一个窗口中执行
# 使用unshare而非ip netns创建一个network namespace
$ sudo unshare -n /bin/bash
root@longshuai-vm:/home/longshuai# echo $$
5094

# 在第二个窗口中执行
# 现在这个network namespace就像是由ip netns创建一样
$ sudo ip netns attach ns3 5094

 

ip [-all] netns delete [ NAME ]
删除/vaer/run/netns/下指定的network namespace,如果指定了–all,则删除/var/run/netns下所有的network namespace。注意,它同时会卸载mount bind的挂载点/var/run/netns/NAME并删除该文件。

$ ip netns list
ns3
ns2
ns1 (id: 0)

$ sudo ip netns del ns3
$ ls /var/run/netns
ns1  ns2

$ sudo ip --all netns del
$ ls /var/run/netns

ip netns set NAME NETNSID
为/var/run/netns/NAME对应的network namespace设置ID号。

$ sudo ip netns add ns1
$ ip netns list
ns1

$ sudo ip netns set ns1 11
$ ip netns list
ns1 (id: 11)

ip netns identify [PID]
根据PID,输出该PID所在的network namespace的netns name。

$ ip netns list
ns1 (id: 11)
$ sudo nsenter --net=/var/run/netns/ns1 /bin/bash
root@longshuai-vm:/home/longshuai# echo $$
5298

# 在另一个窗口中查询进程PID=5298在哪一个network namespace中
$ sudo ip netns identify 5298
ns1

ip netns pids NAME
输出networ namespace中当前正在运行的所有进程PID

$ sudo ip netns pids ns1
5298

ip [-all] netns exec [ NAME ] cmd …
在指定的network namespace中执行命令CMD。如果指定了–all选项,则CMD命令将在/var/run/netns/下的所有network namespace中都执行。

注:如果/etc/netns/NAME下有配置文件,执行ip netns exec命令时会自动将其bind到/etc/下对应的配置文件上。此时还需注意,systemd管理的/etc/resolv.conf是一个软链接,ip netns直接bind时会失败,将其移除后再创建普通文件类型的/etc/resolv.conf才可bind成功。

BASH

$ sudo mkdir -p /etc/netns/ns1
$ echo 'nameserver 8.8.8.8' | sudo tee /etc/netns/ns1/resolv.conf


$ sudo ip netns exec ns1 ping www.baidu.com
Bind /etc/netns/ns1/resolv.conf -> /etc/resolv.conf failed: No such file or directory
PING www.baidu.com (36.152.44.96) 56(84) bytes of data.
64 bytes from 36.152.44.96 (36.152.44.96): icmp_seq=1 ttl=127 time=36.2 ms
64 bytes from 36.152.44.96 (36.152.44.96): icmp_seq=2 ttl=127 time=37.5 ms

# 创建普通文件类型的/etc/resolv.conf
$ readlink /etc/resolv.conf
../run/systemd/resolve/stub-resolv.conf
$ sudo mv /etc/resolv.conf{,.bak}
$ sudo touch /etc/resolv.conf
$ sudo ip netns exec ns1 dig -t A www.baidu.com
......
;www.baidu.com.                 IN      A

;; ANSWER SECTION:
www.baidu.com.          581     IN      CNAME   www.a.shifen.com.
www.a.shifen.com.       51      IN      CNAME   www.wshifen.com.
www.wshifen.com.        70      IN      A       104.193.88.77
www.wshifen.com.        70      IN      A       104.193.88.123

;; Query time: 215 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)      # 已经成功使用8.8.8.8作为nameserver
;; WHEN: Sun Oct 11 16:58:51 CST 2020
;; MSG SIZE  rcvd: 127m

理解user namespace

user namespace涉及namespace的权限和安全问题,是内容最多也最复杂的一种namespace。本文不深入太多理论细节,而是只介绍user namespace机制导致的现象,这样可以足够简单地了解user namespace,也可以控制好篇幅。

user namespace是唯一一种不要求root权限就可以创建的namespace。换句话说,如果是非root用户,可以不使用sudo创建user namespace。

BASH

# --user或-U表示创建user namespace
$ unshare -U /bin/bash

# 非root用户创建其他类型的namespace都需要sudo
## 创建uts namespace
# sudo unshare -u /bin/bash

用户A创建user namespace ns1后,在ns1中活动的仍然是用户A。但是,ns1中的用户A有点特殊:它的用户名将变成nobody,UID和GID都为65534,65534对应的用户名和组名分别是nobody、nogroup。

BASH

$ echo $USER
longshuai
$ echo $HOME
/home/longshuai
$ id
uid=1000(longshuai) gid=1000(longshuai) groups=1000(longshuai)
$ readlink /proc/39523/ns/user
user:[4026531837]

# 创建user namespace ns1
$ sudo unshare -U /bin/bash

# 进入了ns1
# $USER看到的用户名仍然为longshuai,家目录也没有变
# 但是uid/gid都变了
# 且whoami和id看到的都是nobody
$ echo $USER
longshuai
$ echo $HOME
/home/longshuai
$ whoami
nobody
$ id
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)
$ readlink /proc/39650/ns/user
user:[4026532588]

 

不仅如此,此时ns1中的文件、目录的owner/group也全都是nobody/nogroup。

BASH

$ echo $HOME
/home/longshuai

$ ls -l ~
total 40
-rw-rw-r-- 1 nobody nogroup    0 Oct  9 18:05 ax.log
drwxr-xr-x 2 nobody nogroup 4096 Jun  6 12:51 Desktop
drwxr-xr-x 2 nobody nogroup 4096 Jun  6 12:51 Documents
drwxr-xr-x 2 nobody nogroup 4096 Jun  6 12:51 Downloads
drwxrwxr-x 6 nobody nogroup 4096 Oct  6 10:10 fs
......

$ ls -l / 
total 2097236
lrwxrwxrwx   1 nobody nogroup          7 Jun  6 12:25 bin -> usr/bin
drwxr-xr-x   8 nobody nogroup       4096 Jun 26 14:18 blog
drwxr-xr-x   4 nobody nogroup       4096 Jul 10 09:22 boot
drwxrwxr-x   2 nobody nogroup       4096 Jun  6 12:26 cdrom
drwxr-xr-x  17 nobody nogroup       4120 Oct  5 20:26 dev
drwxr-xr-x 140 nobody nogroup      12288 Oct  4 10:29 etc
drwxr-xr-x   3 nobody nogroup       4096 Jul  2 11:25 home
......

$ ls -l /etc/*.conf
-rw-r--r-- 1 nobody nogroup  3028 Apr 23 15:32 /etc/adduser.conf
-rw-r--r-- 1 nobody nogroup   769 Jan 19  2020 /etc/appstream.conf
-rw-r--r-- 1 nobody nogroup 26916 Mar  4  2020 /etc/brltty.conf
-rw-r--r-- 1 nobody nogroup  5714 Jun  6 13:16 /etc/ca-certificates.conf
-rw-r--r-- 1 nobody nogroup  2969 Aug  3  2019 /etc/debconf.conf
-rw-r--r-- 1 nobody nogroup   604 Sep 16  2018 /etc/deluser.conf
-rw-r--r-- 1 nobody nogroup   685 Feb 14  2020 /etc/e2scrub.conf
......

 

也就是说,从文件、目录的所有者以及所属组来看,当前的用户(nobody)在ns1中是具有对应权限的。但实际上,user namespace中的权限非常受限:只具备创建user namespace的用户(即longshuai)权限

BASH

$ pwd
/home/longshuai
# 在longshuai家目录以及/tmp下创建文件成功
$ touch ~/temp.txt
$ touch /tmp/a.log

# 不能在/etc、/root、/下创建文件
$ touch /etc/hello.conf
touch: cannot touch '/etc/hello.conf': Permission denied
$ touch /root/temp.txt
touch: cannot touch '/root/temp.txt': Permission denied
$ touch /temp.txt     
touch: cannot touch '/temp.txt': Permission denied

 

所以,原namespace中的用户和当前user namespace中的用户是有对应关系的:默认情况下,创建user namespace的用户映射为新建user namespace中的用户

其实,用户可以指定如何映射用户,而且这通常也是创建user namespace后应该做的第一件事。

UID和GID映射涉及到的文件分别为/proc/<PID>/uid_map/proc/<PID>/gid_map,它们的格式一样,都只能写入一次,第二次写入将报错。在某些老版本(内核4.14及之前的版本)里,这两个文件可能不存在,且最多只能写入5行。

这两个文件的格式为:

PLAINTEXT

ID_child ID_parent length

/proc/13333/uid_map文件为例,假如向该文件写入0 1000 500,这表示将父级user namespace中1000-1500之间的UID逐一映射到PID=13333所在user namespace中0-500的UID。GID的映射方式也一样如此。

尽管这两个map文件的owner和group可能正是当前用户,但是非root用户却无法直接修改该文件。要修改该文件,需要具备cap_setuid和cap_setgid能力(capability)或者直接使用具有最大权限的root用户。

BASH

# 在第一个窗口创建user namespace ns1
$ unshare -U /bin/bash
$ echo $$     # ns1的bash进程号为44101
44101
# 当前用户为longshuai
$ echo $USER
longshuai

# 在第二个窗口修改uid_map和gid_map
# 直接echo '0 1000 500' >/proc/44101/uid_map将报错
# 需使用sudo或者赋予cap_setuid、cap_setgid能力

# 使用sudo设置uid_map,稍后使用capability设置gid_map文件
$ echo '0 1000 500' | sudo tee /proc/44101/uid_map

# 或者如下设置capability
# 表示将setuid和setgid两种能力添加到/bin/bash文件的有效集(e,effective)和许可集(p,permitted)上,还有一个i(inheritable)表示继承集
# +ep表示将能力添加到集合,即提升对应能力
# -ep表示从集合中剔除,即降权
# =ep表示直接设置为此能力
$ sudo setcap cap_setuid,cap_setgid+ep /bin/bash

# 设置后可查看/bin/bash的能力
$ getcap /bin/bash
/bin/bash = cap_setgid,cap_setuid+ep

# 回到第一个窗口的user namespace中,重新执行/bin/bash
# 使得user namespace中的/bin/bash获取已赋予的能力
$ exec bash
# 查看当前bash进程已获取的能力,发现许可位和有效位都设置了值
$ grep 'Cap' /proc/$$/status
CapInh: 0000000000000000
CapPrm: 00000000000000c0  # 这个c位是新增的
CapEff: 00000000000000c0  # 这个c位是新增的
CapBnd: 0000003fffffffff
CapAmb: 0000000000000000

# 回到第二个窗口,在user namespace外修改gid_map
echo '0 1000 500' >/proc/44101/gid_map
 

只要完成了uid和gid的映射,在user namespace中的用户名和组就会改变,其有能力修改的的文件所有者以及所属组也都会改变。

例如上面的示例中是将uid=1000的longshuai用户映射到user namespace中的uid=0(即root)用户。

BASH

 

# 在 user namespace中
$ id
uid=0(root) gid=0(root) groups=0(root),65534(nogroup)

# 有权限操作的文件owner、group也会随之发生改变
$ ls -l /home
total 4
drwxr-xr-x 21 root root 4096 Oct  9 21:57 longshuai

# 无权限操作的文件owner、group不会改变
$ ls -l /etc/*.conf
-rw-r--r-- 1 nobody nogroup ... /etc/adduser.conf
-rw-r--r-- 1 nobody nogroup ... /etc/appstream.conf
-rw-r--r-- 1 nobody nogroup ... /etc/brltty.conf
......

$ ls -l /
total 2097236
lrwxrwxrwx   1 nobody nogroup ... bin -> usr/bin
drwxr-xr-x   8 nobody nogroup ... blog
drwxr-xr-x   4 nobody nogroup ... boot
drwxrwxr-x   2 nobody nogroup ... cdrom
drwxr-xr-x  17 nobody nogroup ... dev
drwxr-xr-x 140 nobody nogroup ... etc
drwxr-xr-x   3 nobody nogroup ... home
......
 

再做一个简单的实验,不要让映射起始UID正好是创建user namespace的用户。

例如,使用UID=1200的xiaofang用户创建user namespace:

BASH

$ sudo useradd -u 1200 -m xiaofang
$ sudo passwd xiaofang
$ su - xiaofang

# uid=1200的xiaofang用户创建user namespace
$ unshare -U /bin/bash
$ echo $$
48441
 

再设置如下映射方式0 1000 500

BASH

# 新建一个会话窗口,在父级user namespace中执行
$ echo '0 1000 500' | sudo tee /proc/48441/uid_map >/dev/null
$ echo '0 1000 500' | sudo tee /proc/48441/gid_map >/dev/null
 

如此设置映射后,user namespace中的用户将变为哪个呢?实际上,谁创建user namespace,谁就是这个user namespace中的活动用户。也就是说,映射之前的UID=1200的xiaofang是user namespace当前的用户。

所以,父级user namespace中UID=1200映射到新的user namespace中就是UID=200:

BASH

# 在新建的user namespace中执行
$ id
uid=200 gid=200 groups=200
 

从id的输出结果看,uid=200的用户不存在,它没有对应的用户名,同理组名也一样不存在。

BASH

# 在新建的user namespace中执行
$ exec bash
groups: cannot find name for group ID 200
 

从这个现象可以分析,在映射用户和组时,最可能也最合理的映射方式是将user namespace的创建者映射为UID=0的root。

因此,对于UID=x的用户创建的user namespace来说,可能需要如下方式映射UID:

BASH

0 x <Length>
 

对于这种最常见的映射方式,unshare提供了一个快捷选项-r, --map-root-user,可以帮助用户在创建user namespace时自动将当前用户映射为user namespace内UID=0的root。

BASH

$ unshare -U -r /bin/bash
$ id
uid=0(root) gid=0(root) groups=0(root),65534(nogroup)
 

当将创建user namespace的用户A映射为root后,在这个新的user namespace中它将具有当前namespace的【所有权】,比如可以直接创建所有类型的namespace。

BASH

$ unshare -U -r /bin/bash

# 映射为root后,可直接创建uts+mount+pid namespace
# 不需要再提权,因为当前就是root
$ unshare -u -p -f -m --mount-proc /bin/bash
 

但实际上这个root权限是非常受限的,因为在和其他user namespace交互时(比如修改父级user namespace的内容),内核仍然使用用户A来做权限判断。

例如,即使映射为root,也无法修改主机名:

BASH

$ unshare -U -r /bin/bash

$ whoami
root
$ hostname ns1
hostname: you must be root to change the host name
 

之所以在user namespace中已经映射为root仍然无法修改主机名,是因为主机名资源是父级uts namespace的内容。其实,任何一个非user namespace类型的namespace,都有一个与之关联的user namespace,这样才能管理这些user namespace的权限。

比如系统启动后,所有初始的非user namespace的namespace,它们所关联的user namespace就是与之同层次的root user namespace。

再例如,在系统启动后创建的任意非user namespace类型的namespace,它们关联的user namespace都是root uset namespace。

再比如,在user namespace ns1中创建uts namespace ns2,ns2关联的user namespace是ns1。

因此可做出总结:创建非user namespace类型的namespace时,这些namespace关联的user namespace是创建者所属的user namespace

因此,如果想要在user namespace中修改主机名,需要在创建user namespace的时候创建uts namespace,或者在user namespace内部创建uts namespace,这样一来,uts namespace所关联的user namespace就是这个新建的user namespace:

BASH

$ unshare -u -U -r /bin/bash
$ whoami
root
$ hostname ns1    #  修改成功
$ hostname
ns1

# 或者
$ unshare -U -r /bin/bash
$ whoami 
root
$ unshare -u /bin/bash
$ hostname ns1  # 也可以修改成功
$ hostname
ns1
 

最后,用户创建的user namespace中无法挂载块设备,块设备的挂载操作只能在关联了初始root user namespace的namespace中挂载。也就是说,只要某个namespace的祖先有一个是用户创建的user namespace,都将无法挂载块设备。但允许挂载以下类型的文件系统:

PLAINTEXT

/proc (since Linux 3.8)
/sys (since Linux 3.8)
devpts (since Linux 3.9)
tmpfs(5) (since Linux 3.9)
ramfs (since Linux 3.9)
mqueue (since Linux 3.9)
bpf (since Linux 4.4)

文章作者: 骏马金龙

文章链接: https://www.junmajinlong.com/virtual/namespace/ns_overview/

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 骏马金龙

“您的支持是我持续分享的动力”

微信收款码
微信
支付宝收款码
支付宝

目录