理解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
|
现在ns0有3个子孙级的pid namespace:ns1、ns2和ns3。
在ns0中查看进程关系:
BASH
|
其中每一对unshare---bash
代表一个pid namespace(但注意,unshare不在其子进程bash所在的pid namespace中)。
BASH
|
这三个bash进程在各自的pid namespace中的PID=1。但从输出结果可知,在ns0中,这三个pid namespace中的PID分别为:
BASH
|
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
|
这表示pid=16540这个进程对应ns1中的pid=25,对应ns2中的pid=9,对应ns3中的pid=1。
同样地,可以进入到ns1中(bash pid=16513)去查看进程pid=25的进程对应关系:
BASH
|
pid namespace和procfs(/proc)※
/proc
目录是内核对外暴露的可供用户查看或修改的内核中所记录的信息,包括内核自身的部分信息以及每个进程的信息。比如对于pid=N的进程来说,它的信息保存在/proc/<N>
目录下。
/proc
是一个挂载点,是伪文件系统procfs的挂载点,文件系统类型是proc:
BASH
|
在操作系统启动的过程中,会挂载procfs到/proc目录,它存在于root namespace中。
但是,创建新的pid namespace时不会自动重新挂载procfs,而是直接拷贝父级namespace的挂载点信息。这使得在新的pid namespace中仍然保留了父级namespace的/proc
目录,也就是在新创建的这个pid namespace中仍然保留了父级的进程信息。
BASH
|
之所以有上述问题,其原因是在pid namespace中保留了root namespace中的/proc目录,而不是属于pid namespace自己的/proc。
但用户创建pid namespace时希望的是有完全独立的进程运行环境。这时,需要在pid namespace中重新挂载procfs,或者在创建pid namespace时指定--mount-proc
选项。
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
|
在pid namespace内部,只能向pid=1的进程发送那些在pid=1的进程中注册了信号处理程序的信号。
例如,如果pid namespace中/bin/bash作为pid=1的进程,那么在此环境中,只能向该bash进程发送设置了trap的信号,其他信号都被直接忽略,这样可以避免无法信号导致pid namespace被自己内部的进程终止。
但对于pid=1的/bin/bash做测试,发现SIGHUP信号总是有效。
BASH
|
在pid namespace外部,只有其祖先级namespace可发送信号给该pid namespace中的进程,因为其他namespace中看不到该pid namespace中的进程信息。但是发送信号时要找准pid namespace中的进程在祖先namespace中的PID值。
例如:
BASH
|
理解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 许可协议。转载请注明来自 骏马金龙!