Linux namespace技术应用实践–调用宿主机命令(tcpdump/ip/ps/top)检查docker容器网络、进程状态

背景

最近偶然听了几堂极客时间的云原生免费公开课程,首次接触到了Linux namespace技术,并了解到这正是现在风头正劲的容器技术基石,引起了自己探究一二的兴趣,结合课程+网络搜索+实践操作,也算有了一些初步的了解,这里记录、总结一些学习过程。

Linux namespace简介

namespace技术网上的介绍已经很多了,这里不做过多赘述,简单总结namespace是Linux 内核提供的支持内核资源隔离的关键技术,目前包含以下7类namespace:
Namespace 变量 隔离资源
Cgroup CLONE_NEWCGROUP Cgroup 根目录
IPC CLONE_NEWIPC System V IPC, POSIX 消息队列等
Network CLONE_NEWNET 网络设备,协议栈、端口等
Mount CLONE_NEWNS 挂载点
PID CLONE_NEWPID 进程ID
User CLONE_NEWUSER 用户和group ID
UTS CLONE_NEWUTS Hostname和NIS域名
本文中主要涉及到的是Network+PID+Mount三个namespace。

容器运行时缺少必要命令问题与解决方案

下载使用docker官方提供的基础操作系统镜像-本例中为deiban–时会发现很多命令都默认没有安装–比如网络抓包tcpdump、甚至进程信息查看ps/top等,直觉上的办法只能进入容器内部逐个安装。然而如果每次运行新容器都需要安装一遍相关工具包的话未免有些繁琐,另外如果只是启动初期临时使用一下这些工具调试,之后便不再需要,额外安装这些工具其实也不必要的增大了容器本身的复杂度。
针对这一问题,其实linux提供了nsenter、unshare命令用于进入容器进程所属Network、PID、Mount 等namespace执行宿主机命令,从而达到无需在容器中安装命令,直接使用宿主机相应命令的目的,以下以tcpdump/ps/top三个命令的执行为例进行进行介绍。

利用宿主机tcpdump命令对docker容器进行抓包

利用nsenter命令可以指定目标namespace,并在其中执行对应命令。
以下命令先运行一个debian基础镜像的容器,而后在其中执行ip addr命令查看网络配置,并尝试执行tcpdump命令抓包

~# docker run -it --name ns_test_net   -d debian:stretch
d221b13a5fbcbf23a981a3067847b743081fff20ae05e6892b8744546cb1b148
~# docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
d221b13a5fbc        debian:stretch      "bash"              9 seconds ago       Up 6 seconds                            ns_test_net
~# docker exec -it ns_test_net bash
root@d221b13a5fbc:/# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
    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
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1
    link/ipip 0.0.0.0 brd 0.0.0.0
23: eth0@if24: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever
root@d221b13a5fbc:/# tcpdump dump -i any -nvv
bash: tcpdump: command not found

可以看到报错command not found,此时可以简单通过nsenter使用宿主机命令进入容器所属namespace执行相关命令:
通过ip addr 查看容器网络配置,通过tcpdump 尝试抓包

~# docker inspect -f {{.State.Pid}} ns_test_net # 获取容器进程在宿主机上的pid
9164
 nsenter -t 9164 -n ip addr # -t指定容器进程pid,-n指定使用对应pid的Network namespace, 执行ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
    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
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1
    link/ipip 0.0.0.0 brd 0.0.0.0
23: eth0@if24: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever
~# nsenter -t 9164 -n tcpdump -nvv -i any # 使用宿主机tcpdump命令对容器所属Network namespace抓包,注意需要同时在宿主机上执行:ping 172.17.0.3
tcpdump: listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes

17:07:50.290707 ARP, Ethernet (len 6), IPv4 (len 4), Request who-has 172.17.0.3 tell 172.17.0.1, length 28
17:07:50.290743 ARP, Ethernet (len 6), IPv4 (len 4), Reply 172.17.0.3 is-at 02:42:ac:11:00:03, length 28
17:07:50.290761 IP (tos 0x0, ttl 64, id 22629, offset 0, flags [none], proto ICMP (1), length 84)
    172.17.0.1 > 172.17.0.3: ICMP echo request, id 10895, seq 1, length 64
17:07:50.290777 IP (tos 0x0, ttl 64, id 9364, offset 0, flags [none], proto ICMP (1), length 84)
    172.17.0.3 > 172.17.0.1: ICMP echo reply, id 10895, seq 1, length 64
17:07:51.307360 IP (tos 0x0, ttl 64, id 22696, offset 0, flags [none], proto ICMP (1), length 84)
    172.17.0.1 > 172.17.0.3: ICMP echo request, id 10895, seq 2, length 64
17:07:51.307397 IP (tos 0x0, ttl 64, id 9365, offset 0, flags [none], proto ICMP (1), length 84)
    172.17.0.3 > 172.17.0.1: ICMP echo reply, id 10895, seq 2, length 64
17:07:52.331317 IP (tos 0x0, ttl 64, id 22867, offset 0, flags [none], proto ICMP (1), length 84)
    172.17.0.1 > 172.17.0.3: ICMP echo request, id 10895, seq 3, length 64
17:07:52.331352 IP (tos 0x0, ttl 64, id 9564, offset 0, flags [none], proto ICMP (1), length 84)
    172.17.0.3 > 172.17.0.1: ICMP echo reply, id 10895, seq 3, length 64
17:07:53.355311 IP (tos 0x0, ttl 64, id 22927, offset 0, flags [none], proto ICMP (1), length 84)

利用宿主机ps/top工具对docker容器内运行进程进行状态监控

首次尝试

根据前面使用nsenter进入Network namespace执行网络相关命令的经验,很容易得出使用nsenter进入PID namespace空间执行ps/top等命令即可获取容器内进程状态的想法,然而实际执行后会发现:

~# nsenter -t 9164 -p ps -elf
F S UID         PID   PPID  C PRI  NI ADDR SZ WCHAN  STIME TTY          TIME CMD
4 S root          1      0  0  80   0 - 34783 SyS_ep  2021 ?        00:20:18 /sbin/init
1 S root          2      0  0  80   0 -     0 -       2021 ?        00:00:02 [kthreadd]
1 S root          3      2  0  80   0 -     0 -       2021 ?        00:04:39 [ksoftirqd/0]
1 S root          5      2  0  60 -20 -     0 -       2021 ?        00:00:00 [kworker/0:0H]
1 S root          7      2  0  80   0 -     0 rcu_gp  2021 ?        01:46:22 [rcu_sched]
1 S root          8      2  0  80   0 -     0 -       2021 ?        00:00:00 [rcu_bh]
1 S root          9      2  0 -40   - -     0 -       2021 ?        00:03:31 [migration/0]
1 S root         10      2  0  60 -20 -     0 -       2021 ?        00:00:00 [lru-add-drain]

ps 实际显示的还是宿主机当前的进程信息(1号进程为/sbin/init),而非容器内部的进程信息,top也是一样的效果,这是为什么呢?

失败原因分析

//github.com/util-linux/util-linux/issues/660 解释到:

The command nsenter just enters the namespace(s), and nothing else. The behaviour of the utils like ps(1) depend on environment in the namespace. It's out of nsenter business to setup the environment (for example mount /proc). Maybe docker also uses mount namespace in the container, in this care you also need to enter --mount namespace etc.

大意是nsenter实际做的只是进入对应的namespace,而ps这些进程监控工具实际上依赖namespace中的环境设置–如/proc文件系统,nsenter并不会负责这些环境设置工作,所以需要使用者自己负责–比如mount /proc系统等。
在 man ps中实际也可以找到:

This ps works by reading the virtual files in /proc.  This ps does not need to be setuid kmem or have any privileges to run.  Do not give this ps any special permissions.

明确说明了ps依赖于/proc文件系统执行实际工作。
通过man proc 简单看一下什么是proc filesystem:

The proc filesystem is a pseudo-filesystem which provides an interface to kernel data structures.  It is commonly mounted at /proc.  Most of it is read-only, but some files allow kernel variables to be changed.

这是一个提供内核数据结构访问接口的伪文件系统,一般挂载在/proc路径下,那么之前使用nsenter -t 9164 -p 实际只是进入了PID namespace,但是使用的Mount namespace依然属于宿主机,所以ps/top这些工具依然读取的是宿主机的/proc文件,所以其输出的内容自然也就是宿主机进程执行信息了,为了解决这个问题我们需要让ps能够读取到容器拥有的proc文件系统。

同时进入PID+Mount namespace

第一个直觉反应是直接使用nsenter同时进入PID+Mount namespace,想当然既然都已经进入了容器的Mount namespace, 那ps命令自然读取的就是容器的/proc路径了,执行以下命令:

~# nsenter -t 9164 -p -m ps -elf
nsenter: failed to execute ps: No such file or directory

发现报 No such file or directory,思考后得出结论,既然都是用容器的 Mount namespace了,那ps命令的执行路径也就变成了容器内的文件系统了,而容器本身并没有安装ps命令,自然也就会报找不到文件了,所以同时挂载Mount namespace执行命令成功的前提是容器内部本来也已经安装了对应命令–这很明显无法满足我们的需求。

使用nsenter重新单独挂载proc文件系统

退一步考虑,只重新挂载proc文件系统可否呢,尝试通过 nsenter进入容器namespace 后先mount proc而后执行ps:

:~# nsenter -t 9164 -p
~# ps -lf # 输出结果为宿主机上进程状态
F S UID         PID   PPID  C PRI  NI ADDR SZ WCHAN  STIME TTY          TIME CMD
4 S root       8742   8719  0  80   0 -  5306 -      16:50 pts/2    00:00:00 -bash
4 S root      18449   8742  0  80   0 -  3694 -      18:10 pts/2    00:00:00 nsenter -t 9164 -p
0 S root      18450  18449  0  80   0 -  5305 -      18:10 pts/2    00:00:00 -bash
0 R root      18462  18450  0  80   0 -  9576 -      18:10 pts/2    00:00:00 ps -lf
~# mount -t proc proc /proc
~# ps -lf # 重新mount proc后执行结果为容器内进程状态
F S UID         PID   PPID  C PRI  NI ADDR SZ WCHAN  STIME TTY          TIME CMD
0 S root         40      0  0  80   0 -  5305 -      18:10 pts/2    00:00:00 -bash
0 R root         50     40  0  80   0 -  9576 -      18:10 pts/2    00:00:00 ps -lf
~# top # top执行结果类似
top - 18:10:21 up 279 days,  3:47,  7 users,  load average: 0.00, 0.00, 0.00
Tasks:   3 total,   1 running,   2 sleeping,   0 stopped,   0 zombie
%Cpu(s):  1.1 us,  0.0 sy,  0.0 ni, 98.9 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem : 12355128 total,  1187052 free,  5869104 used,  5298972 buff/cache
KiB Swap:        0 total,        0 free,        0 used.  6130300 avail Mem

   PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
     1 root      20   0   18120   3100   2672 S   0.0  0.0   0:00.03 bash
    40 root      20   0   21220   5044   3140 S   0.0  0.0   0:00.02 bash
    51 root      20   0   44824   3540   3028 R   0.0  0.0   0:00.00 top

咋一看,通过nsenter进入PID namespace且重新mount proc文件系统后,ps与top输出的都已经是容器内的进程信息了,一切问题好像都解决了?
但是此时如果回到宿主机中执行ps与top会发现出问题了:

~# ps
Error, do this: mount -t proc proc /proc
~# top
Error, do this: mount -t proc proc /proc

这是因为nsenter中用的依然是宿主机的Mount namespace,这种情况下重新mount proc改变的是宿主机Mount namespace的状态,于是在nsenter内部使用容器的PID namespace+容器的proc系统工作正常,但是在宿主机上使用宿主机的PID namespace+容器的proc系统就会出错了。
通过 findmnt -o+PROPAGATION 命令可以查看当前mount状态,正常的proc状态如下:

~# findmnt -o+PROPAGATION

而nsenter 重新mount proc系统,宿主机再次mount proc后会变成这样

~# findmnt -o+PROPAGATION


于是直接通过nsenter 进入容器PID namespace 并重新mount proc的方法能够正常执行容器内的ps/top等命令,但是却有影响宿主机正常行为的副作用,不可取。
看起来需要探究一个既能在容器中正确执行宿主机的ps、top命令而又不能有影响宿主机状态副作用的方法,在网上查了不少资料还真没找到一个明确的方案,忍不住思考、摸索了数日,终于自己想出了一个目前看来能正确work的方案–引入unshare。

使用unshare+nsenter单独挂载proc文件系统

通过 unshare 命令可以在原进程上进行 namespace 隔离,也就是创建并加入新的 namespace,我们考虑先通过unshare命令将宿主机Mount namespace进行隔离,而后在隔离后进程中再次执行nsenter 进入容器的PID namespace,并重新挂载proc文件系统,这样新挂载的proc系统只会影响unshare的子进程,而不会穿透到宿主机之上。
具体在宿主机上执行以下命令:

~# unshare -fm
~# nsenter -t 9164 -p
~# mount -t proc proc /proc
~# ps -elf
F S UID         PID   PPID  C PRI  NI ADDR SZ WCHAN  STIME TTY          TIME CMD
4 S root          1      0  0  80   0 -  4530 core_s 16:53 pts/0    00:00:00 bash
0 S root         72      0  0  80   0 -  5305 -      18:48 pts/2    00:00:00 -bash
0 R root         81     72  0  80   0 -  9576 -      18:48 pts/2    00:00:00 ps -elf
~# top
top - 18:49:17 up 279 days,  4:26,  7 users,  load average: 0.00, 0.04, 0.06
Tasks:   3 total,   1 running,   2 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.5 us,  0.2 sy,  0.0 ni, 99.3 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem : 12355128 total,  1036408 free,  5898532 used,  5420188 buff/cache
KiB Swap:        0 total,        0 free,        0 used.  6100836 avail Mem

   PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
     1 root      20   0   18120   3100   2672 S   0.0  0.0   0:00.03 bash
    72 root      20   0   21220   5064   3160 S   0.0  0.0   0:00.02 -bash
    82 root      20   0   44824   3496   2992 R   0.0  0.0   0:00.00 top
~# findmnt -o+PROPAGATION


可以看到ps/top输出的正是容器内的进程信息,同时findmnt结果中可以看到proc系统的PROPAGATION已经变成了private,表明mount变更不会影响其他namespace的状态。
回过来在宿主机上执行以下命令验证其proc系统不受影响:

~# ps -elf | head
F S UID         PID   PPID  C PRI  NI ADDR SZ WCHAN  STIME TTY          TIME CMD
4 S root          1      0  0  80   0 - 34783 SyS_ep  2021 ?        00:20:18 /sbin/init
1 S root          2      0  0  80   0 -     0 -       2021 ?        00:00:02 [kthreadd]
1 S root          3      2  0  80   0 -     0 -       2021 ?        00:04:39 [ksoftirqd/0]
1 S root          5      2  0  60 -20 -     0 -       2021 ?        00:00:00 [kworker/0:0H]
1 S root          7      2  0  80   0 -     0 rcu_gp  2021 ?        01:46:24 [rcu_sched]
1 S root          8      2  0  80   0 -     0 -       2021 ?        00:00:00 [rcu_bh]
1 S root          9      2  0 -40   - -     0 -       2021 ?        00:03:31 [migration/0]
1 S root         10      2  0  60 -20 -     0 -       2021 ?        00:00:00 [lru-add-drain]
5 S root         11      2  0 -40   - -     0 -       2021 ?        00:00:36 [watchdog/0]
~# findmnt -o+PROPAGATION

转载请注明出处,原文地址://www.cnblogs.com/AcAc-t/p/host_command_execution_in_linux_container.html

参考

//zhuanlan.zhihu.com/p/73248894
//unix.stackexchange.com/questions/124162/reliable-way-to-jail-child-processes-using-nsenter/124194#124194
//www.cnblogs.com/mrhelloworld/p/docker11.html
//www.zhihu.com/question/24964878
//github.com/util-linux/util-linux/issues/660