Linux namespace技術應用實踐--呼叫宿主機命令(tcpdump/ip/ps/top)檢查docker容器網路、程序狀態

2022-05-30 12:08:15

背景

最近偶然聽了幾堂極客時間的雲原生免費公開課程,首次接觸到了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也是一樣的效果,這是為什麼呢?

失敗原因分析

https://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

轉載請註明出處,原文地址:https://www.cnblogs.com/AcAc-t/p/host_command_execution_in_linux_container.html

參考

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