老板有个奇怪的需求,通过一个 kubeconfig 文件,获取主机的各种状态信息,比如进程列表、进程状态等。
第一反应就是,老板是不是不懂容器,容器怎么能这样玩,这样玩还要什么容器,内心万马奔腾。
直到最近遇到了一个命令行工具,才发现原来小丑是我自己。下面一起来看看,我发现了什么吧。
沙箱是一个虚拟环境,在沙箱内部进行的操作对外部没有影响。沙箱与沙箱之间是隔离的,也是不可见的,看不到彼此的存在。
我们常说的容器就是基于 Linux 的 Cgroups 和 Namespace 技术构建的一个沙箱环境。
从图中,可以看到,容器与容器的边界就是通过 Cgroups 和 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。
上面将一组进程放置到一个 Namespace,对外隔离,对内共享资源,接着使用 Cgroups 对其进行资源的控制。Cgroups 提供了四个功能:
利用 Namespcae 和 Cgroups 提供的沙箱环境,再加上文件系统技术,就支持起了容器技术。
nsenter 是一个用来进入指定程序,所在 Namespace,并执行命令的工具。在容器场景下,很多容器为了轻量化,而裁剪了很多基础的命令,比如 ip
、 tcpdump
等。这样给调试容器带来了一定的困难,通过 nsenter 共享 Namespace 进行调试,可以很好地解决这个问题。
实际上,RunC 在创建容器时,也是调用的 nsenter ,在 libcontainer 的代码中可以看到。
大部分的 Linux 操作系统,已经内置了 nsenter 命令。如果没有,以 CentOS 为例,执行如下命令,安装 util-linux
包即可:
1 | yum install -y util-linux |
---|
由于不同的 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 |
---|
1 2 3 | nsenter -V nsenter from util-linux 2.23.2 |
---|
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 的功能,我们在容器环境下进一步实验。
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=1 的进程,如同宿主机上的 init 进程一样,是其他进程的父进程。但是在主机上,容器进程具有另外一个 PID ,可以用于管理容器。
1 2 3 | docker inspect --format "{{ .State.Pid }}" 9addecf82c5e 3969 |
---|
这里以进入网络空间为例:
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
命令却来自宿主机。
1 | docker run --privileged --net=host --ipc=host --pid=host -it --rm docker.io/alpine:3.12 /bin/sh |
---|
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 |
---|
从执行结果可以看到,命令显示的是主机上的容器信息,但却是在容器下执行的命令。
这部分的内容和上一个章节类似,只不过在进入容器时,需要借道 Pod 获取 PID;在主机上执行命令时,需要借道 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> |
---|
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 |
---|
1 2 3 | docker inspect --format "{{.State.Pid}}" 981c94ef07abfbeca548e9e36cd70a7369d1cf38a50754c2dc4f87fbc27601d1 6954 |
---|
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 之后,需要切换到所在主机进行操作。
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 |
---|
1 | kubectl apply -f pod-test.yaml |
---|
1 | kubectl exec -it pod-test /bin/sh |
---|
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,直接执行主机上的命令。
本篇主要介绍了在容器环境下,如何逃逸到主机执行命令;在主机下,如何进入容器调试环境。同时,还给出了在 Container 和 Kubernetes 两种场景下的实践示例。
其中有两点对我有所启发,一个是 nsenter 命令,加深了对容器技术的理解。另外一个是特权模式启动的容器,权限十分大,需要谨慎,业务应该尽量采用 rootless 的方式运行容器。
在以特权模式启动的 Docker Daemon 中,创建 Kuberntes 集群,通过 nsenter 命令,可以 nodeSelector 到任意节点,然后执行 kubectl/docker/systemctl 等命令进行破坏活动。
原文最新更新链接:https://www.chenshaowen.com/blog/operate-host-in-container-and-debug-container-on-host.html
更多精彩内容,请关注公众号。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。