接着上文 VPC网络架构下的网络上数据采集
介绍
考虑一个监控系统,它的数据采集 Agent 是以 daemonset 形式运行在物理机上的,它需要采集 Pod 的各种监控信息。现在很流行的一个监控信息是通过 Prometheus 提供指标信息。
一般来说,daemonset Agent 会使用物理机网络(就算有自己的网络也没关系),它需要通过 ICMP、TCP、HTTP 等协议访问目标 Pod。这在一般的 k8s 场景下是没问题的,因为此时 daemonset Agent 与 目标 Pod 之间的网络是通的,没有隔离。
而在一些公有云或多租户环境下, 一个物理机上实际分给了好几个租户,此时网络安全管理员往往会限制物理机网络与 Pod 网络是隔离的。这么一搞我们原本的整套链路都不工作了。当然,网络安全管理员这么做是师出有名,并且确实是必要的。但这种方案确实没有给监控场景留一条活路,我们也很难受。
本文分享几种方案从不同角度突破了这种隔离,当然它们并不一定通用,更多的是为监控场景服务。
可能涉及到的技术或概念
- Input / Output Stream
- command exec
- Linux namespace 、nsenter
- docker exec / docker SDK
- unix domain socket / abstract unix domain socket, 以下简称 uds 或 abstract uds
- socat
基于 Linux namespace 的方案
如果你使用的是标准的 docker/containerd/runc 或 containerd/runc 的组合,并且对 docker、runc 的实现原理有一些了解,那么你应该听说过 Linux namespace。本文不再介绍 Linux namespace 与 docker、runc 的关系。
Agent 想与 pod:ip 建立 TCP 连接,只需要将当前线程 network namespace 切换到目标 pod 的 network namespace,这样对于当前线程来说它就进入了目标 pod 的网络,此时你再执行 `net.Dial` 那就能连接上了.
可以参考下面链接的代码.
https://github.com/traas-stack/holoinsight-agent/blob/main/pkg/cri/cricore/nsenter_linux.go#L52
linux 有个命令叫 nsenter 本质它就是直接切换的 namespace 然后执行命令, 感兴趣的可以看一下.
基于 docker sdk exec 的方案
这里的 docker sdk exec 指的是通过 docker sdk 执行 exec 命令, 而非命令行里的 docker exec. 这样做效率更高一些.
基于 Linux namespace 的方案只能适用于 runc 容器运行时. 遇到其他容器运行时就不工作了.
但无论怎么变,底层的 CRI 实现总是会提供类似 docker exec、containerd exec 的功能。
我们可以想办法往目标容器里放一个辅助流量转发的 binary,这个 bianry 从 stdin 读数据,然后写到目标 pod:port, 从目标 pod:port 读数据, 然后写到 stdout. 通过 docker exec 执行该 binary 让其转发流量.
可以参考如下链接的代码
https://github.com/traas-stack/holoinsight-agent/blob/main/pkg/cri/criutils/tcpproxy.go#L31
如果直接用 docker sdk exec 方案的话,它对代码的修改比较大。需要配合基于 HTTP 代理 或 socks5 代理才能减少对代码的侵入性。
docker sdk exec 提供了一种通用的方式用于请求转发, 在我遇到的所有场景里, 它都可以工作. 当然由于它是基于命令行执行的, 因此执行效率会低一些, 使用的时候要考虑这一点.
考虑到 k8s 提出了自己的 CRI 规范, 而不再强依赖 docker/containerd 的实现API. 因此我们之后应该直接面向 CRI 编程.
但是 CRI 也有一些缺点:
- CRI 的初始化步骤比较麻烦, 不像 docker 只需要指定其 unix domain sock 地址就行了.
- CRI 的 exec 无法指定 user/workdir/env
某些特定容器运行时有建立 proxy 的功能
在实践中,某些以安全为特点的容器运行时也考虑到了网络隔离问题,因此它们往往具备建立 Proxy 的能力。
以某个基于 VM 的容器运行时为例。
它会有 2个 netns: VM 有一个,Pod 有一个。
VM 的 netns 我们可以从 docker inspect 的信息中看到,而 Pod 的 netns 我们无法看到(也无法访问,不在一个 Linux 内核里)。
该容器运行时提供了一个命令,它可以连接 VM netns 和 Pod netns 里的 2 个 abstract uds。甚至可以做到连接 2 个 netns 里的 ip:port (其实就是做一层转发).
Agent 可以通过 setns 的方式进入 VM 的 netns,再利用上述命令即可打通 Agent 到 目标 Pod 的 ip:port。
创建 proxy 时需要选择方向:
- VM 主动建连 Pod,需要在 Pod 上 Listen
- Pod 主动建连 VM,需要在 VM 上 Listen
基于 HTTP 代理的方案
假设 Agent 访问 Pod 用的是 HTTP 协议。
我们在 Agent 上实现一个 HTTP 代理服务器,Agent 访问 Pod 时配置 HTTP proxy,这样流量就会发到 HTTP 代理服务器上。在该代理服务器的实现里,我们读取请求序列化为 []byte,通过 docker exec 将该请求序列化的 []byte 作为 stdin 传入调用容器内部的辅助程序。辅助程序启动后从 stdin 读数据并反序列化出 HTTP 请求,辅助程序在容器内部访问 http://pod:port 是一定可以通的。辅助程序将 http://pod:port 的返回结果写到 stdout 里。HTTP 代理服务器实现读 docker exec 的 output 转成对应的 http.Response 即可。
可以参考下面链接的代码.
https://github.com/traas-stack/holoinsight-agent/blob/main/pkg/cri/impl/netproxy/proxy_http.go#L118
基于 socks5 代理的方案
比 HTTP 更通用的场景是 TCP 级的代理,常见的是 socks5 代理。
比如你想要通过 mysql 命令连到隔离的目标容器里,mysql 讲的显然是它自己的协议不是 HTTP,此时 TCP 级别的代理就派上用场了。我们只需要在 mysql 建连的地方设置 socks5 proxy 就行。
刚好有一个开源软件 socat 可以从 标准流 双向拷贝到一个 TCP 流, 所以我上面的图用的是 socat. 但实际情况中, 目标容器里并不一定有 socat. 所以实际情况中我们会复制一个辅助 binary 到容器内部, 该 binary 实现了类似的功能.
可以参考下面链接的代码.https://github.com/traas-stack/holoinsight-agent/blob/main/pkg/cri/impl/netproxy/proxy_socks5.go