Please enable Javascript to view the contents

如何在主机上调试容器、在容器中操作主机

 ·  ☕ 6 分钟

1. 一个奇怪的需求

老板有个奇怪的需求,通过一个 kubeconfig 文件,获取主机的各种状态信息,比如进程列表、进程状态等。

第一反应就是,老板是不是不懂容器,容器怎么能这样玩,这样玩还要什么容器,内心万马奔腾。

直到最近遇到了一个命令行工具,才发现原来小丑是我自己。下面一起来看看,我发现了什么吧。

2. 容器的原理

沙箱是一个虚拟环境,在沙箱内部进行的操作对外部没有影响。沙箱与沙箱之间是隔离的,也是不可见的,看不到彼此的存在。

我们常说的容器就是基于 Linux 的 Cgroups 和 Namespace 技术构建的一个沙箱环境。

从图中,可以看到,容器与容器的边界就是通过 Cgroups 和 Namespace 这两种技术控制的。下面简单描述一下这两种技术:

  1. Namespace

不同 Namespace 下的资源相互独立、不可见。Linux 从 2.4.19 完成了支持 Mount Namespace,2.6.19 完成了支持 UTS、IPS Namespace,2.6.24 完成了支持 PID Namespace,2.6.29 完成了支持 Network Namespace,3.8 完成了支持 User Namespace 。其中,除了 User Namespace ,其他都需要以 root 权限创建。同时,在 4.6 中已经新增了 Cgroup namespace,目前 RunC(Docker 提供的运行时) ,已经合并了相关的 PR: https://github.com/opencontainers/runc/pull/1916 。下面是其中的 7 种 Namespace。

  • Mount namespace,隔离文件系统挂载点。一个 Namespace 中,程序对文件的修改,只影响自身的文件系统,而对其他 Namespace 没有影响。

  • UTS namespace,隔离主机名和域名信息。每个 Namespace 中,主机和域名信息相互独立。

  • IPC namespace,隔离进程通信的行为。只有一个 Namespace 中的进程可以互相通信。

  • PID namespace,隔离进程的 PID 空间。不同 Namespace 中的进程 PID 可以重复,互不影响。PID 为 1 的进程是其他所有进程的父进程,因此这个 Namespace 非常有意义。

  • Network namespace,隔离网络资源。每个 Namespace 都具有独立的网络栈信息,容器运行时仿佛在一个独立的网络中。

  • User namespace,隔离用户和用户组。同一个用户在不同的 Namespce,可以具有不同的角色,用来保障安全性。

  • Cgroup namespace,隔离 Cgroup 的可见性。每个 Namespace 中,都具有独立的 cgroupns root 和 cgroup filesystem 视图。

  1. Cgroups

上面将一组进程放置到一个 Namespace,对外隔离,对内共享资源,接着使用 Cgroups 对其进行资源的控制。Cgroups 提供了四个功能:

  • 资源限制。针对一个进程或进程组,设置资源消耗限制。比如内存超出限制,会导致申请内存失败。
  • 资源统计。统计 CPU 使用时长、内存用量等。
  • 任务控制。控制进程的状态,可以挂起、恢复进程。
  • 优先级分配。设置进程的优先级。

利用 Namespcae 和 Cgroups 提供的沙箱环境,再加上文件系统技术,就支持起了容器技术。

3. 一个调试工具: nsenter

nsenter 是一个用来进入指定程序,所在 Namespace,并执行命令的工具。在容器场景下,很多容器为了轻量化,而裁剪了很多基础的命令,比如 iptcpdump 等。这样给调试容器带来了一定的困难,通过 nsenter 共享 Namespace 进行调试,可以很好地解决这个问题。

实际上,RunC 在创建容器时,也是调用的 nsenter ,在 libcontainer 的代码中可以看到。

  1. 安装 nsenter

大部分的 Linux 操作系统,已经内置了 nsenter 命令。如果没有,以 CentOS 为例,执行如下命令,安装 util-linux 包即可:

1
yum install -y util-linux
  1. nsenter 的版本和参数

由于不同的 Linux Kernel 对 Namespace 支持的程度不一样,nsenter 的版本会有所差异。这里以 CentOS 7 为例:

  • 查看系统内核版本
1
2
3
uname -a

Linux i-x29a8rdc 3.10.0-1127.10.1.el7.x86_64 #1 SMP Wed Jun 3 14:28:03 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
  • 查看 nsenter 版本
1
2
3
nsenter -V

nsenter from util-linux 2.23.2
  • 查看 nsenter 的参数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
nsenter -h

Usage:
 nsenter [options] <program> [<argument>...]

Run a program with namespaces of other processes.

Options:
 -t, --target <pid>     target process to get namespaces from 
 -m, --mount[=<file>]   enter mount namespace
 -u, --uts[=<file>]     enter UTS namespace (hostname etc)
 -i, --ipc[=<file>]     enter System V IPC namespace
 -n, --net[=<file>]     enter network namespace
 -p, --pid[=<file>]     enter pid namespace
 -U, --user[=<file>]    enter user namespace
 -S, --setuid <uid>     set uid in entered namespace
 -G, --setgid <gid>     set gid in entered namespace
     --preserve-credentials do not touch uids or gids
 -r, --root[=<dir>]     set the root directory
 -w, --wd[=<dir>]       set the working directory
 -F, --no-fork          do not fork before exec'ing <program>
 -Z, --follow-context   set SELinux context according to --target PID

 -h, --help     display this help and exit
 -V, --version  output version information and exit

这里需要注意的是 -t 参数,指定一个进程,用于获取 Namepace 参数。其他参数主要是使能、设置参数。

由于非沙箱环境下,并不容易体现 nsenter 的功能,我们在容器环境下进一步实验。

4. nsenter 在 Docker 容器环境下的应用

4.1 主机下,进入容器的 Namespace 环境

  • 选择一个容器
1
2
3
4
docker ps

CONTAINER ID        IMAGE                       COMMAND                  CREATED             STATUS                          PORTS                    NAMES
9addecf82c5e        sonarqube:7.9.4-community   "./bin/run.sh"           3 weeks ago         Up 3 weeks                      0.0.0.0:9000->9000/tcp   sonarqube_sonarqube_1
  • 获取容器的 PID

每个容器内都有一个 PID=1 的进程,如同宿主机上的 init 进程一样,是其他进程的父进程。但是在主机上,容器进程具有另外一个 PID ,可以用于管理容器。

1
2
3
docker inspect --format "{{ .State.Pid }}" 9addecf82c5e

3969
  • 进入容器的 Namespace 环境

这里以进入网络空间为例:

1
nsenter -t 3969  -n /bin/bash

如果宿主机上的默认 shell,在容器中存在,可以省略 /bin/bash,否则需要显式指定一个容器中的 shell。

  • 执行主机上的命令行工具,调试容器环境
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
ip addr

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
11: eth0@if12: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:12:00:03 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.18.0.3/16 brd 172.18.255.255 scope global eth0
       valid_lft forever preferred_lft forever

从执行结果可以看到,显示的是容器上的网卡地址信息,但 ip 命令却来自宿主机。

4.2 容器下,进入主机的 Namespace 环境

  • 以特权模式,使用主机的 Namespace 创建容器
1
docker run --privileged --net=host --ipc=host --pid=host -it --rm docker.io/alpine:3.12 /bin/sh
  • 进入 PID=1 进程的 Namespace 环境
1
nsenter -t 1 -m -u -i -n
  • 执行命令,操作主机环境
1
2
3
4
5
docker ps

CONTAINER ID        IMAGE                       COMMAND                  CREATED              STATUS                          PORTS                    NAMES
2cd99b9d7b5a        alpine:3.12                 "/bin/sh"                About a minute ago   Up About a minute                                        trusting_khorana
9addecf82c5e        sonarqube:7.9.4-community   "./bin/run.sh"           3 weeks ago          Up 3 weeks                      0.0.0.0:9000->9000/tcp   sonarqube_sonarqube_1

从执行结果可以看到,命令显示的是主机上的容器信息,但却是在容器下执行的命令。

5. nsenter 在 Kubernetes 容器环境下的应用

这部分的内容和上一个章节类似,只不过在进入容器时,需要借道 Pod 获取 PID;在主机上执行命令时,需要借道 Pod 创建容器。

5.1 从主机进入 Kubernetes Pod 中,调试容器环境

  • 选择一个 Pod
1
2
3
4
kubectl get pod -o wide

NAME                     READY   STATUS             RESTARTS   AGE     IP             NODE               NOMINATED NODE   READINESS GATES
nginx-6db489d4b7-589bd   1/1     Running            0          11s     10.233.76.91   tf-cd-allinone-0   <none>           <none>
  • 获取容器 ID
1
2
3
kubectl describe pod nginx-6db489d4b7-589bd | grep -A10 "^Containers:" | grep -Eo 'docker://.*$' | head -n 1 | sed 's/docker:\/\/\(.*\)$/\1/'

981c94ef07abfbeca548e9e36cd70a7369d1cf38a50754c2dc4f87fbc27601d1
  • 切换到主机所在节点
1
ssh root@tf-cd-allinone-0
  • 获取容器的 PID
1
2
3
docker inspect --format "{{.State.Pid}}" 981c94ef07abfbeca548e9e36cd70a7369d1cf38a50754c2dc4f87fbc27601d1

6954
  • 进入容器的 Namespace
1
nsenter -t 6954 -n
  • 执行主机命令,调试容器
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
ip addr

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
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000
    link/ipip 0.0.0.0 brd 0.0.0.0
4: eth0@if100: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1440 qdisc noqueue state UP group default
    link/ether 16:a3:44:dc:58:ce brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.233.76.91/32 scope global eth0
       valid_lft forever preferred_lft forever

这里需要注意的是,容器和节点是绑定在一起的,对于多节点环境,获取容器 ID 之后,需要切换到所在主机进行操作。

5.2 在 Kubernetes Pod 中,直接操作主机

  • 新建一个 pod-test.yaml 文件,内容如下
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
apiVersion: v1
kind: Pod
metadata:
  name: pod-test
  namespace: default
spec:
  containers:
    - command: ['sh', '-c', 'echo "Hello, wwww.chenshaowen.com !" && sleep 3600']
      image: docker.io/alpine:3.12
      name: pod-test
      securityContext:
        privileged: true
  hostIPC: true
  hostNetwork: true
  hostPID: true
  • 创建 Pod
1
kubectl apply -f pod-test.yaml
  • 进入 Pod 中
1
kubectl exec -it pod-test /bin/sh
  • 进入 PID=1 进程的 Namespace
1
nsenter -t 1 -m -u -i -n -p
  • 执行主机命令测试
1
2
3
4
5
6
docker ps

CONTAINER ID   IMAGE                                         COMMAND                   CREATED         STATUS         PORTS     NAMES
f6bd778c3172   389fef711851                                  "sh -c 'echo \"Hello,…"   4 minutes ago   Up 4 minutes             k8s_pod-test_pod-test_default_3a496075-419e-477a-b03c-a423677a90be_0
4e197fd98294   kubesphere/pause:3.1                          "/pause"                  4 minutes ago   Up 4 minutes             k8s_POD_pod-test_default_3a496075-419e-477a-b03c-a423677a90be_0
...

以特权模式启动容器,通过 PID=1 的进程共享 Namespace,直接执行主机上的命令。

6. 总结

本篇主要介绍了在容器环境下,如何逃逸到主机执行命令;在主机下,如何进入容器调试环境。同时,还给出了在 Container 和 Kubernetes 两种场景下的实践示例。

其中有两点对我有所启发,一个是 nsenter 命令,加深了对容器技术的理解。另外一个是特权模式启动的容器,权限十分大,需要谨慎,业务应该尽量采用 rootless 的方式运行容器。

在以特权模式启动的 Docker Daemon 中,创建 Kuberntes 集群,通过 nsenter 命令,可以 nodeSelector 到任意节点,然后执行 kubectl/docker/systemctl 等命令进行破坏活动。

7. 参考


微信公众号
作者
微信公众号