重定向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有两种运行模式:特权模式和非特权模式。首先看下非特权模式。

非特权模式

非特权模式的运行逻辑为:

  1. 找到本地的tcpdump可执行文件路径
  2. 将本地的tcpdump上传到远端pod中
  3. 远程执行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支持三种常见的容器运行时:dockercri-ocontainerd,对应的容器运行时的默认目录如下:

/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,导致该方法行不通。

尽管存在一些使用上的限制,但本文在文件上传以及对不同容器运行时方面的处理还是很值得借鉴的。

相关实践学习
深入解析Docker容器化技术
Docker是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。Docker是世界领先的软件容器平台。开发人员利用Docker可以消除协作编码时“在我的机器上可正常工作”的问题。运维人员利用Docker可以在隔离容器中并行运行和管理应用,获得更好的计算密度。企业利用Docker可以构建敏捷的软件交付管道,以更快的速度、更高的安全性和可靠的信誉为Linux和Windows Server应用发布新功能。 在本套课程中,我们将全面的讲解Docker技术栈,从环境安装到容器、镜像操作以及生产环境如何部署开发的微服务应用。本课程由黑马程序员提供。     相关的阿里云产品:容器服务 ACK 容器服务 Kubernetes 版(简称 ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情: https://www.aliyun.com/product/kubernetes
目录
相关文章
|
11月前
|
存储 Kubernetes 调度
【赵渝强老师】什么是Kubernetes的Pod
Pod 是 Kubernetes 中的基本逻辑单位,代表集群上的一个应用实例。它可以由一个或多个容器组成,并包含数据存储和网络配置等资源。Pod 支持多种容器执行环境,如 Docker。Kubernetes 使用 Pod 管理容器,具有简化部署、方便扩展和调度管理等优点。视频讲解和图示详细介绍了 Pod 的组成结构和使用方式。
220 1
|
Kubernetes API 调度
k8s中节点无法启动Pod
【10月更文挑战第3天】
425 6
|
7月前
|
Kubernetes Docker 容器
Kubernetes与Docker参数对照:理解Pod中的command、args与Dockerfile中的CMD、ENTRYPOINT。
需要明确的是,理解这些都需要对Docker和Kubernetes有一定深度的理解,才能把握二者的区别和联系。虽然它们都是容器技术的二个重要组成部分,但各有其特性和适用场景,理解它们的本质和工作方式,才能更好的使用这些工具,将各自的优点整合到生产环境中,实现软件的快速开发和部署。
240 25
|
11月前
|
Prometheus Kubernetes 监控
深入探索Kubernetes中的Pod自动扩展(Horizontal Pod Autoscaler, HPA)
深入探索Kubernetes中的Pod自动扩展(Horizontal Pod Autoscaler, HPA)
|
7月前
|
Kubernetes Shell Windows
【Azure K8S | AKS】在AKS的节点中抓取目标POD的网络包方法分享
在AKS中遇到复杂网络问题时,可通过以下步骤进入特定POD抓取网络包进行分析:1. 使用`kubectl get pods`确认Pod所在Node;2. 通过`kubectl node-shell`登录Node;3. 使用`crictl ps`找到Pod的Container ID;4. 获取PID并使用`nsenter`进入Pod的网络空间;5. 在`/var/tmp`目录下使用`tcpdump`抓包。完成后按Ctrl+C停止抓包。
244 12
|
11月前
|
存储 Kubernetes Docker
【赵渝强老师】Kubernetes中Pod的基础容器
Pod 是 Kubernetes 中的基本单位,代表集群上运行的一个进程。它由一个或多个容器组成,包括业务容器、基础容器、初始化容器和临时容器。基础容器负责维护 Pod 的网络空间,对用户透明。文中附有图片和视频讲解,详细介绍了 Pod 的组成结构及其在网络配置中的作用。
187 1
【赵渝强老师】Kubernetes中Pod的基础容器
|
11月前
|
弹性计算 Kubernetes Perl
k8s 设置pod 的cpu 和内存
在 Kubernetes (k8s) 中,设置 Pod 的 CPU 和内存资源限制和请求是非常重要的,因为这有助于确保集群资源的合理分配和有效利用。你可以通过定义 Pod 的 `resources` 字段来设置这些限制。 以下是一个示例 YAML 文件,展示了如何为一个 Pod 设置 CPU 和内存资源请求(requests)和限制(limits): ```yaml apiVersion: v1 kind: Pod metadata: name: example-pod spec: containers: - name: example-container image:
1427 2
|
11月前
|
运维 Kubernetes Shell
【赵渝强老师】K8s中Pod的临时容器
Pod 是 Kubernetes 中的基本调度单位,由一个或多个容器组成,包括业务容器、基础容器、初始化容器和临时容器。临时容器用于故障排查和性能诊断,不适用于构建应用程序。当 Pod 中的容器异常退出或容器镜像不包含调试工具时,临时容器非常有用。文中通过示例展示了如何使用 `kubectl debug` 命令创建临时容器进行调试。
200 1
|
11月前
|
Kubernetes 调度 容器
【赵渝强老师】K8s中Pod中的业务容器
Pod 是 Kubernetes 中的基本调度单元,由一个或多个容器组成。除了业务容器,Pod 还包括基础容器、初始化容器和临时容器。本文通过示例介绍如何创建包含业务容器的 Pod,并提供了一个视频讲解。示例中创建了一个名为 "busybox-container" 的业务容器,并使用 `kubectl create -f firstpod.yaml` 命令部署 Pod。
150 1
|
11月前
|
Kubernetes 容器 Perl
【赵渝强老师】K8s中Pod中的初始化容器
Kubernetes的Pod包含业务容器、基础容器、初始化容器和临时容器。初始化容器在业务容器前运行,用于执行必要的初始化任务。本文介绍了初始化容器的作用、配置方法及优势,并提供了一个示例。
200 1

热门文章

最新文章

推荐镜像

更多