重定向Kubernetes pod中的tcpdump输出
重定向Kubernetes pod中的tcpdump输出
最新发现一个比较有意思的库ksniff,它是一个kubectl 插件,使用tcpdump来远程捕获Kubernetes集群中的pod流量并保存到文件或输出到wireshark中,发布网络问题定位。使用方式如下:
kubectl sniff hello-minikube-7c77b68cff-qbvsd -c hello-minikube
要知道很多pod中其实是没有tcpdump这个可执行文件的,那它是如何在Kubernetes集群的Pod中远程执行tcpdump命令的?又是如何倒出Pod的tcpdump的输出并将输出直接传递给wireshark的?下面分析一下该工具的实现方式。
ksniff有两种运行模式:特权模式和非特权模式。首先看下非特权模式。
非特权模式
非特权模式的运行逻辑为:
- 找到本地的tcpdump可执行文件路径
- 将本地的tcpdump上传到远端pod中
- 远程执行pod的tcpdump命令,并将输出重定向到文件或wireshark
上传tcpdump可执行文件
ksniff使用tar命令对tcpdump可执行文件进行打包,然后通过client-go的remotecommand库将其解压到pod中,最后执行tcpdump命令即可:
fileContent, err := ioutil.ReadFile(req.Src) //读取tcpdump可执行文件
if err != nil {
return 0, err
}
tarFile, err := WrapAsTar(destFileName, fileContent)//将使用tar命令对tcpdump进行打包
if err != nil {
return 0, err
}
stdIn := bytes.NewReader(tarFile) //通过标准输入传递给容器
tarCmd := []string{"tar", "-xf", "-"} //构建解压命令
destDir := path.Dir(req.Dst)
if len(destDir) > 0 {
tarCmd = append(tarCmd, "-C", destDir)
}
execTarRequest := ExecCommandRequest{
KubeRequest: KubeRequest{
Clientset: req.Clientset,
RestConfig: req.RestConfig,
Namespace: req.Namespace,
Pod: req.Pod,
Container: req.Container,
},
Command: tarCmd,
StdIn: stdIn,
StdOut: stdOut,
StdErr: stdErr,
}
exitCode, err := PodExecuteCommand(execTarRequest)
tar打包的实现如下:
func WrapAsTar(fileNameOnTar string, fileContent []byte) ([]byte, error) {
var buf bytes.Buffer
tw := tar.NewWriter(&buf)
hdr := &tar.Header{
Name: fileNameOnTar,
Mode: 0755,
Size: int64(len(fileContent)),
}
if err := tw.WriteHeader(hdr); err != nil {
return nil, err
}
if _, err := tw.Write(fileContent); err != nil {
return nil, err
}
if err := tw.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
远程执行命令
下面是远程在pod中执行命令的代码,是client-go remotecommand库的标准用法,没有什么特别之处:
func (k *KubernetesApiServiceImpl) ExecuteCommand(podName string, containerName string, command []string, stdOut io.Writer) (int, error) {
log.Infof("executing command: '%s' on container: '%s', pod: '%s', namespace: '%s'", command, containerName, podName, k.targetNamespace)
stdErr := new(Writer)
executeTcpdumpRequest := ExecCommandRequest{
KubeRequest: KubeRequest{
Clientset: k.clientset,
RestConfig: k.restConfig,
Namespace: k.targetNamespace,
Pod: podName,
Container: containerName,
},
Command: command,
StdErr: stdErr,
StdOut: stdOut,
}
exitCode, err := PodExecuteCommand(executeTcpdumpRequest)
if err != nil {
log.WithError(err).Errorf("failed executing command: '%s', exitCode: '%d', stdErr: '%s'",
command, exitCode, stdErr.Output)
return exitCode, err
}
log.Infof("command: '%s' executing successfully exitCode: '%d', stdErr :'%s'", command, exitCode, stdErr.Output)
return exitCode, err
}
func PodExecuteCommand(req ExecCommandRequest) (int, error) {
execRequest := req.Clientset.CoreV1().RESTClient().Post().
Resource("pods").
Name(req.Pod).
Namespace(req.Namespace).
SubResource("exec")
execRequest.VersionedParams(&corev1.PodExecOptions{
Container: req.Container,
Command: req.Command,
Stdin: req.StdIn != nil,
Stdout: req.StdOut != nil,
Stderr: req.StdErr != nil,
TTY: false,
}, scheme.ParameterCodec)
exec, err := remotecommand.NewSPDYExecutor(req.RestConfig, "POST", execRequest.URL())
if err != nil {
return 0, err
}
err = exec.Stream(remotecommand.StreamOptions{
Stdin: req.StdIn,
Stdout: req.StdOut, //重定向的输出,可以是文件或wireshark
Stderr: req.StdErr,
Tty: false,
})
var exitCode = 0
if err != nil {
if exitErr, ok := err.(utilexec.ExitError); ok && exitErr.Exited() {
exitCode = exitErr.ExitStatus()
err = nil
}
}
return exitCode, err
}
执行tcpdump命令
该步骤就是组装远程命令,并在目标pod中执行即可:
func (u *StaticTcpdumpSnifferService) Start(stdOut io.Writer) error {
log.Info("start sniffing on remote container")
command := []string{u.settings.UserSpecifiedRemoteTcpdumpPath, "-i", u.settings.UserSpecifiedInterface,
"-U", "-w", "-", u.settings.UserSpecifiedFilter}
exitCode, err := u.kubernetesApiService.ExecuteCommand(u.settings.UserSpecifiedPodName, u.settings.UserSpecifiedContainer, command, stdOut)
if err != nil || exitCode != 0 {
return errors.Errorf("executing sniffer failed, exit code: '%d'", exitCode)
}
log.Infof("done sniffing on remote container")
return nil
}
wireshark库支持输入重定向,使用o.wireshark.StdinPipe()创建出输入之后,将其作为远程调用tcpdump命令的StreamOptions.Stdout的参数即可将pod的输出重定向到wireshark中:
title := fmt.Sprintf("gui.window_title:%s/%s/%s", o.resultingContext.Namespace, o.settings.UserSpecifiedPodName, o.settings.UserSpecifiedContainer)
o.wireshark = exec.Command("wireshark", "-k", "-i", "-", "-o", title)
stdinWriter, err := o.wireshark.StdinPipe() //创建输入
if err != nil {
return err
}
go func() {
err := o.snifferService.Start(stdinWriter)//将wireshark创建的输入作为pod的输出
if err != nil {
log.WithError(err).Errorf("failed to start remote sniffing, stopping wireshark")
_ = o.wireshark.Process.Kill()
}
}()
err = o.wireshark.Run()
特权模式
特权模式的处理有一些复杂,该模式下,ksniff会在目标pod所在的node节点(通过目标pod的pod.Spec.NodeName字段获取)上创建一个权限为privileged的pod,并挂载主机的/目录和默认的容器socket,然后在特权pod内调用对应的容器运行时命令来执行tcpdump命令。ksniff支持三种常见的容器运行时:docker、cri-o和containerd,对应的容器运行时的默认目录如下:
/var/run/docker.sock
/var/run/crio/crio.sock
/run/containerd/containerd.sock
由于特权模式可能会创建一个新的pod,因此在命令执行完之后需要清理掉新建的pod。
区分容器运行时
特权模式下会调用目标节点上的容器运行时命令,不同容器运行时的命令是不同的,那么ksniff是如何区分不同的容器运行时呢?
ksniff会通过kubernetes clientset来获取目标pod信息,通过pod.status.containerStatuses.containerID字段来确定所使用的CRI,如下例,其CRI为containerd,containerId为0f76ee399228ed02f8ba13a6bbec6bb8b696f4f1997176882b309edbe3a56ee1:
status:
containerStatuses:
- containerID: containerd://0f76ee399228ed02f8ba13a6bbec6bb8b696f4f1997176882b309edbe3a56ee1
....
容器运行时和ContainerId的获取方式如下:
func (o *Ksniff) findContainerId(pod *corev1.Pod) error {
for _, containerStatus := range pod.Status.ContainerStatuses {
if o.settings.UserSpecifiedContainer == containerStatus.Name {
result := strings.Split(containerStatus.ContainerID, "://")
if len(result) != 2 {
break
}
o.settings.DetectedContainerRuntime = result[0] //获取容器运行时
o.settings.DetectedContainerId = result[1] //获取containerID
return nil
}
}
return errors.Errorf("couldn't find container: '%s' in pod: '%s'", o.settings.UserSpecifiedContainer, o.settings.UserSpecifiedPodName)
}
不同运行时执行tcpdump命令
下面看下不同运行时是如何执行tcpdump命令的。
Containerd
Containerd会在特权pod内通过crictl pull来拉取tcpdump镜像并启动tcpdump容器,使其和目标容器(containerId)共享相同的网络命名空间,这样就可以使用tcpdump抓取目标容器的报文。在命令执行完之后需要清理创建出来的tcpdump容器。
func (d *ContainerdBridge) BuildTcpdumpCommand(containerId *string, netInterface string, filter string, pid *string, socketPath string, tcpdumpImage string) []string {
d.tcpdumpContainerName = "ksniff-container-" + utils.GenerateRandomString(8)
d.socketPath = socketPath
tcpdumpCommand := fmt.Sprintf("tcpdump -i %s -U -w - %s", netInterface, filter)
shellScript := fmt.Sprintf(`
set -ex
export CONTAINERD_SOCKET="%s"
export CONTAINERD_NAMESPACE="k8s.io"
export CONTAINER_RUNTIME_ENDPOINT="unix:///host${CONTAINERD_SOCKET}"
export IMAGE_SERVICE_ENDPOINT=${CONTAINER_RUNTIME_ENDPOINT}
crictl pull %s >/dev/null
netns=$(crictl inspect %s | jq '.info.runtimeSpec.linux.namespaces[] | select(.type == "network") | .path' | tr -d '"')
exec chroot /host ctr -a ${CONTAINERD_SOCKET} run --rm --with-ns "network:${netns}" %s %s %s
`, d.socketPath, tcpdumpImage, *containerId, tcpdumpImage, d.tcpdumpContainerName, tcpdumpCommand)
command := []string{"/bin/sh", "-c", shellScript}
return command
}
func (d *ContainerdBridge) BuildCleanupCommand() []string {
shellScript := fmt.Sprintf(`
set -ex
export CONTAINERD_SOCKET="%s"
export CONTAINERD_NAMESPACE="k8s.io"
export CONTAINER_ID="%s"
chroot /host ctr -a ${CONTAINERD_SOCKET} task kill -s SIGKILL ${CONTAINER_ID}
`, d.socketPath, d.tcpdumpContainerName)
command := []string{"/bin/sh", "-c", shellScript}
return command
}
Cri-o
Cri-o通过nsenter指定目标容器的进程进入目标网络命名空间来执行tcpdump命令,由于它没有使用tcpdump镜像,因此要求目标节点上需要存在tcpdump可执行文件:
func (c *CrioBridge) BuildTcpdumpCommand(containerId *string, netInterface string, filter string, pid *string, socketPath string, tcpdumpImage string) []string {
return []string{"nsenter", "-n", "-t", *pid, "--", "tcpdump", "-i", netInterface, "-U", "-w", "-", filter}
}
这种方式下没有在特权pod内部创建容器,因此不需要清理环境。
docker
docker的处理方式和containerd类似,也是通过启动tcpdump容器,并和目标容器共享网络命名空间实现的:
func (d *DockerBridge) BuildTcpdumpCommand(containerId *string, netInterface string, filter string, pid *string, socketPath string, tcpdumpImage string) []string {
d.tcpdumpContainerName = "ksniff-container-" + utils.GenerateRandomString(8)
containerNameFlag := fmt.Sprintf("--name=%s", d.tcpdumpContainerName)
command := []string{"docker", "--host", "unix://" + socketPath,
"run", "--rm", "--log-driver", "none", containerNameFlag,
fmt.Sprintf("--net=container:%s", *containerId), tcpdumpImage, "-i",
netInterface, "-U", "-w", "-", filter}
d.cleanupCommand = []string{"docker", "--host", "unix://" + socketPath,
"rm", "-f", d.tcpdumpContainerName}
return command
}
func (d *DockerBridge) BuildCleanupCommand() []string {
return d.cleanupCommand
}
环境清理
由于特权模式下创建了特权pod,containerd和docker还会在特权pod内创建tcpdump容器,因此在进行环境清理时需要清理掉创建出来的tcpdump容器,然后再清理掉特权pod:
func (p *PrivilegedPodSnifferService) Cleanup() error {
command := p.runtimeBridge.BuildCleanupCommand()
if command != nil {
log.Infof("removing privileged container: '%s'", p.privilegedContainerName)
exitCode, err := p.kubernetesApiService.ExecuteCommand(p.privilegedPod.Name, p.privilegedContainerName, command, &kube.NopWriter{})
if err != nil {
log.WithError(err).Errorf("failed to remove privileged container: '%s', exit code: '%d', "+
"please manually remove it", p.privilegedContainerName, exitCode)
} else {
log.Infof("privileged container: '%s' removed successfully", p.privilegedContainerName)
}
}
if p.privilegedPod != nil {
log.Infof("removing pod: '%s'", p.privilegedPod.Name)
err := p.kubernetesApiService.DeletePod(p.privilegedPod.Name)
if err != nil {
log.WithError(err).Errorf("failed to remove pod: '%s", p.privilegedPod.Name)
return err
}
log.Infof("pod: '%s' removed successfully", p.privilegedPod.Name)
}
return nil
}
总结
非特权模式的实现比较简单,不需要考虑容器运行时的问题,但它也有一个缺点,就是需要考虑目标容器的运行环境,比如32位/64位、amd/arm等,可能需要在本地准备多套tcpdump来满足不同的容器运行环境。
特权模式的实现相对比较复杂,如果还有其他的运行时,就需要对ksniff进行功能扩展。且有些集群节点上可能会禁用特权pod,导致该方法行不通。
尽管存在一些使用上的限制,但本文在文件上传以及对不同容器运行时方面的处理还是很值得借鉴的。
重定向Kubernetes pod中的tcpdump输出的更多相关文章
- Android 重定向 init.rc中服务的输出
在init.rc中运行的服务,由于系统启动的时候将标准输出重定向到了/dev/null, 所以服务中的打印信息都不可见. 但调试时可能需要看到其中的打印信息,因此就有了logwrapper这个工具:l ...
- Kubernetes Pod中容器的Liveness、Readiness和Startup探针
我最新最全的文章都在南瓜慢说 www.pkslow.com,欢迎大家来喝茶! 1 探针的作用 在Kubernetes的容器生命周期管理中,有三种探针,首先要知道,这探针是属于容器的,而不是Pod: 存 ...
- Kubernetes Pod故障归类与排查方法
Pod概念 Pod是kubernetes集群中最小的部署和管理的基本单元,协同寻址,协同调度. Pod是一个或多个容器的集合,是一个或一组服务(进程)的抽象集合. Pod中可以共享网络和存储(可以简单 ...
- Kubernetes Pod 全面知识
Pod 是在 Kubernetes 中创建和管理的.最小的可部署的计算单元,是最重要的对象之一.一个 Pod 中包含一个或多个容器,这些容器在 Pod 中能够共享网络.存储等环境. 学习 Kubern ...
- Python文件中将print的输出内容重定向到变量中
有时候需要用到别人的代码, 但是又不想修改别人的文件, 想拿到输出的结果, 这时候就需要使用sys模块, 将print输出的内容重定向到变量中. Python调用sys模块中的sys.stdout, ...
- 【K8S学习笔记】Part3:同一Pod中多个容器间使用共享卷进行通信
本文将展示如何使用共享卷(Volume)来实现相同Pod中的两个容器间通信. 注意:本文针对K8S的版本号为v1.9,其他版本可能会有少许不同. 0x00 准备工作 需要有一个K8S集群,并且配置好了 ...
- 同一个POD中默认共享哪些名称空间
如果通过POD的形式来启动多个容器那么它们的名称空间会是共享的么,所以我这里讨论是在默认情况下同一个POD的不同容器的哪些名称空间是打通的.这里先说一下结论,共享的是UTS.IPC.NET.USER. ...
- Python Django撸个WebSSH操作Kubernetes Pod(下)- 终端窗口自适应Resize
追求完美不服输的我,一直在与各种问题斗争的路上痛并快乐着 上一篇文章Django实现WebSSH操作Kubernetes Pod最后留了个问题没有解决,那就是terminal内容窗口的大小没有办法调整 ...
- 解决Kubernetes Pod故障的5个简单技巧
在很多情况下,你可能会发现Kubernetes中的应用程序没有正确地部署,或者没有正常地工作.今天这篇文章就提供了如何去快速解决这类故障以及一些技巧. 在阅读了这篇文章之后,你还将深入了解Kubern ...
- 聊聊 Kubernetes Pod or Namespace 卡在 Terminating 状态的场景
这个话题,想必玩过kubernetes的同学当不陌生,我会分Pod和Namespace分别来谈. 开门见山,为什么Pod会卡在Terminationg状态? 一句话,本质是API Server虽然标记 ...
随机推荐
- SPPS完整版下载安装教程【博主亲测】
SPSS全称Statistical Product and Service Solutions,是一款优秀的数据分析软件,旨在为用户提供专业的统计产品与服务解决方案.SPSS面向行业应用人员所设,被广 ...
- Go实现常用软件设计模式一:单例模式
目录: 举个栗子 概念介绍 使用场景 1.举个栗子 类图 plantuml ``` @startuml'https://plantuml.com/class-diagramclass Elephant ...
- Go语言核心36讲31
我们在前两篇文章中讨论了互斥锁.读写锁以及基于它们的条件变量,先来总结一下. 互斥锁是一个很有用的同步工具,它可以保证每一时刻进入临界区的goroutine只有一个.读写锁对共享资源的写操作和读操作则 ...
- EdgeCore初学习
### 前提 初学edgeCore,有不足之处,欢迎指正 ### 大纲 1. 日志查看2. 重启3. 在线编译4. sftp同步代码5. 整体架构6. 通信协议7. 模拟实现(待实现) ### 步骤 ...
- Selenium4+Python3系列(九) - 上传文件及滚动条操作
一.上传文件操作 上传文件是每个做自动化测试同学都会遇到,而且可以说是面试必考的问题,标准控件我们一般用send_keys()就能完成上传, 但是我们的测试网站的上传控件一般为自己封装的,用传统的上传 ...
- 修改Listen 1源码的一点心得
注:本文只作为技术交流 首先感谢听1的作者写出这么强大的音乐播放器!! 软件首页地址:点击打开链接 软件的github上上上地址:点击打开链接 软件唯一让我美中不足的就是不能下载,这可能是作者考虑到了 ...
- 7. PyQt5 中的多线程的使用(下)
专栏地址 ʅ(‾◡◝)ʃ 紧接着上一节, 这一节具体介绍 图形化界面 如何给 任务线程传递数据 7.1 from form import Ui_Form from PyQt5.QtWidgets im ...
- UEFI引导linux启动过程的顺序,及修改办法
注意这里我说的是 uefi 启动 启动过程顺序 正常安装完一个Linux系统,硬盘会被分成俩个分区,一块是 fat32 文件系统(启动项存放位置),一块是ext4 文件系统(也就是系统真正的安装位置) ...
- Oracle12c异常关闭后启动PDBORCL(ORA-01033)
这个问题已经困扰了我好几天找解决方案,终于找到: 由于Oracle12c的特殊性,但许多用户并不想在创建用户时前面要加"C##" 那么就要创建PDBORCL数据库,来与Oracle ...
- 【实时数仓】Day04-DWS层业务:DWS设计、访客宽表、商品主题宽表、流合并、地区主题表、FlinkSQL、关键词主题表、分词
一.DWS层与DWM设计 1.思路 之前已经进行分流 但只需要一些指标进行实时计算,将这些指标以主题宽表的形式输出 2.需求 访客.商品.地区.关键词四层的需求(可视化大屏展示.多维分析) 3.DWS ...