go程序添加远程调用tcpdump功能

最近开发的telemetry采集系统上线了。听起来高大上,简单来说就是一个grpc/udp服务端,用户的机器(路由器、交换机)将它们的各种统计数据上报采集、整理后交后端的各类AI分析系统分析。目前华为/思科的大部分设备支持。

go程序添加远程调用tcpdump功能

上线之后,各类用户开始找来要求帮忙定位问题,一般是上报的数据在后端系统中不存在等等。

在一通抓包分析后,百分之99都是用户自己的问题。但频繁的抓包定位问题,严重的压缩了我摸鱼的时间。而且,这套系统采用多实例方式部署在腾X云多个容器中,一个个的登录抓包,真的很烦。

这让我萌生了一个需求:

  1. 主动给采集器下发抓包任务。
  2. 将抓包的信息返回。
  3. 将抓包的文件暂存,以备进一步定位问题。

方法1

使用fabric等ssh运维工具,编写脚本自动化登录机器后执行tcpdump,然后进一步处理。
很可惜的是,并没有容器母机ssh的权限。只能通过一个web命令行观察容器。这条路玩不转。

方法2

  1. 在采集器中添加一个接口,用以下发tcpdump命令
  2. 采集器执行tcpdump命令,并获取返回的信息(比如captured xxx pacs),保存相关文件。
  3. 将获取的抓包信息以某种方式反发给命令下发人。

使用tcpdump定时抓取并保存信息

首先需要解决tcpdump定时的问题,以免tcpdump无限期的执行抓包,经过一通谷歌,命令如下:

timeout 30 tcpdump -i eth0 host 9.123.123.111 and port 6651 -w /tmp/log.cap

timeout 30 指抓取30秒,超时后tcpdump会直接退出
-i 指定抓取的端口
host xxx 源IP
port xxx 源端口

编写tcpdump函数

下面到了我最喜欢的写代码阶段,为了简单,直接使用 os/exec库。不要笑,很多大厂的很多系统其实都是包命令行工具,解决问题最重要。

// TcpDump 执行tcpdump命令,并返回抓到的包数
func TcpDump(sudo bool, timeout int, eth string, host string, port int) (caps int, err error) {
    portStr := ""
    if port != 0 {
        portStr = fmt.Sprintf("and port %v", port)
    }

    tcpdumpCmd := fmt.Sprintf("timeout %v tcpdump -i %v host %v %v -w /tmp/log.cap",
        timeout, eth, host, portStr)
    if sudo {
        tcpdumpCmd = "sudo " + tcpdumpCmd
    }
    logrus.Infof("call %v", tcpdumpCmd)
    cmd := exec.Command("sh", "-c", tcpdumpCmd)
    var outb, errb bytes.Buffer
    cmd.Stderr = &errb
    err = cmd.Run()
    if err != nil {
        if !errors.Is(err, &exec.ExitError{}) {
            logrus.Infof("out:%s ; %s", outb.Bytes(), errb.Bytes())
            return getPacs(errb.String()), nil
        }
        return
    }
    return 0,fmt.Errorf("unknown error")
}

func getPacs(input string) int {
    end := strings.Index(input, "packets captured")
    pos := end
    for {
        pos -= 1
        if pos <= 0 { return } if input[pos]="=" '\n' break logrus.infof("captured:%s", input[pos+1:end-1]) v, err :="strconv.Atoi(input[pos+1" end-1]) !="nil" v < code></=>

这里要注意几点:

  1. 执行 cmd := exec.Command("sh", "-c", tcpdumpCmd)后,tcpdump的返回信息类似:
listening on eth1, link-type EN10MB (Ethernet), capture size 65535 bytes\n56 packets captured\n56 packets received by filter\n0 packets dropped by kernel\n

是在stderr中的。而不是stdout。

  1. getPacs函数简单的从 xx packets received中提取出了抓包数。但是如果是中文的服务器系统(不会吧,不会吧),就不太好使了。

编写api

现在函数已经有了,只要再写一个http api,就能很方便的把它暴露出去。

import "github.com/gogf/gf/v2/encoding/gjson"

// ErrJson&#xFF0C;&#x5199;&#x5165;&#x4E00;&#x4E2A;error json&#xFF0C;&#x5F62;&#x5982;&#xFF1A;
//{
//    "err": code,
//    "err_msg": msg
//}
func ErrJson(w http.ResponseWriter, errCode int, errStr string) error {
    w.Header().Set("Content-Type", "application/json")
    js := make(map[string]interface{})
    js["err"] = errCode
    js["err_msg"] = errStr
    jsBts, _ := json.Marshal(js)
    _, err := w.Write(jsBts)
    return err
}

/* TcpDumpHandler
req:{
    "sudo":  true,
    "eth": "eth0",
    "host": "10.99.17.135",
    "port": 0
}
rsp:{
    "err": 0,
    "caps": 14
}
*/
func TcpDumpHandler(w http.ResponseWriter, r *http.Request) {
    r.ParseForm()
    ret, err := ioutil.ReadAll(r.Body)
    if err != nil {
        ErrJson(w, 1, "&#x6570;&#x636E;&#x9519;&#x8BEF;")
        return
    }
    js := gjson.New(ret)
    sudo := js.Get("sudo").Bool()
    eth := js.Get("eth").String()
    if eth == "" {
        ErrJson(w, 1, "&#x6570;&#x636E;&#x9519;&#x8BEF;, eth&#x4E0D;&#x5B58;&#x5728;")
        return
    }
    host := js.Get("host").String()
    if host == "" {
        ErrJson(w, 1, "&#x6570;&#x636E;&#x9519;&#x8BEF;, host&#x4E0D;&#x5B58;&#x5728;")
        return
    }
    port := js.Get("port").Int()
    timeout := js.Get("timeout").Int()
    if timeout == 0 {
        ErrJson(w, 1, "&#x6570;&#x636E;&#x9519;&#x8BEF;, timeout&#x4E3A;0&#x6216;&#x4E0D;&#x5B58;&#x5728;")
        return
    }
    go func() {
        chatKey := config.GlobalConfigObj.Global.ChatKey
        botKey := config.GlobalConfigObj.Global.BotKey

        // &#x8FD9;&#x91CC;&#x76F4;&#x63A5;&#x5229;&#x7528;&#x4E86;&#x516C;&#x53F8;&#x7684;&#x4E00;&#x4E2A;&#x6D88;&#x606F;&#x7CFB;&#x7EDF;&#xFF0C;&#x5982;&#x679C;&#x8D35;&#x516C;&#x53F8;&#x6CA1;&#x6709;&#x8FD9;&#x6837;&#x7684;&#x7CFB;&#x7EDF;&#xFF0C;&#x5C31;&#x53D8;&#x901A;&#x4E00;&#x4E0B;
        msgSender := msg.NewNiuBiMsg(chatKey, botKey)

        caps, err := TcpDump(sudo, timeout, eth, host, port)
        if err != nil {
            return
        }
        if caps > 0 {
            // &#x8FD9;&#x91CC;&#x76F4;&#x63A5;&#x5229;&#x7528;&#x4E86;&#x516C;&#x53F8;&#x7684;&#x4E00;&#x4E2A;&#x6D88;&#x606F;&#x7CFB;&#x7EDF;&#xFF0C;&#x5411;&#x4F01;&#x4E1A;IM&#x53D1;&#x4E00;&#x6761;&#x6D88;&#x606F;
            msgSender.Send(fmt.Sprintf("tcpdump agent_ip:%v host:%v eth:%v port:%v, captured:%v",
                config.GlobalLocalConfig.LocalIP, host, eth, port, caps))
            bts, err := ioutil.ReadFile("/tmp/log.cap")
            if err != nil {
                return
            }
            b64Caps := base64.StdEncoding.EncodeToString(bts)
            // &#x628A;&#x6293;&#x5305;&#x7684;&#x6587;&#x4EF6;&#x901A;&#x8FC7;&#x8FD9;&#x4E2A;&#x6D88;&#x606F;&#x7CFB;&#x7EDF;&#x4E5F;&#x53D1;&#x5230;&#x4F01;&#x4E1A;IM&#x4E2D;
            msgSender.File(fmt.Sprintf("pacs_%v.cap", config.GlobalLocalConfig.LocalIP), b64Caps)
        }
    }()
}

然后起一个http svr

func runHttp() {
    mux := http.NewServeMux()
    server :=
        http.Server{
            Addr:         fmt.Sprintf(":%d", 3527),
            Handler:      mux,
            ReadTimeout:  3 * time.Second,
            WriteTimeout: 5 * time.Second,
        }
    // &#x5F00;&#x59CB;&#x6DFB;&#x52A0;&#x8DEF;&#x7531;
    mux.HandleFunc("/tcpdump", tcpdumpsvc.TcpDumpHandler)
    logrus.Infof("run http:%v", 3527)
    logrus.Info(server.ListenAndServe())
}

到这一步,这个系统就基本完成了。使用这个命令就能调用接口。

curl --header "Content-Type: application/json" --request GET --data '{"sudo":false,"eth":"eth0","host":"100.xxx.xxx.10","port":0,"timeout":5}' http://0.0.0.0:3527/tcpdump

这个系统有几个硬伤。

  1. 依赖了公司的消息系统完成抓包数据回发的功能。假如各位大佬的公司没有这样的系统 msgSender.Send,可行的方法有:
  2. scp到一个特定的文件夹。
  3. 使用电子邮件。
  4. 和领导申请自己开发一套,你看,需求就来了。
  5. tcpdump可能会生成极大的抓包文件,此时使用 bts, err := ioutil.ReadFile("/tmp/log.cap"),可能会直接让系统OOM。所以设置timeout和抓包的大小(比如在tcpdump命令中使用 -c)是很重要的。换句话说,这个api不是公有的,别让不了解的人去调用。

不过这都是小问题。现在用户找上门来,我只需要启动脚本,从服务发现api拉到所有的实例IP,然后依次调用tcpdump api,等待IM的反馈即可。又能快乐的摸鱼啦。

Original: https://www.cnblogs.com/superpigwin/p/16271664.html
Author: superpigwin
Title: go程序添加远程调用tcpdump功能

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/516293/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • sync:一. 原子操作(atomic)

    原子操作是指在程序运行中不能被中断的操作,原子操作是无锁的常常是由CPU指令直接实现,而锁一般由操作系统的调度器实现,所以原子操作的效率一般更高。 golang中原子操作支持的类型…

    Go语言 2023年5月25日
    050
  • 【golang】分布式缓存 – lru算法实现

    最近复习操作系统,看到了lru算法,就去网上搜索下,因此发现了GeeCache,顺手写了一遍。研究下lru算法的实现。 正文: lru使用map+链表实现。map里面存储了key以…

    Go语言 2023年5月25日
    033
  • 自己实现一个Controller——精简型

    写在最前 controller-manager作为K8S master的其中一个组件,负责众多controller的启动和终止,这些controller负责监控着k8s中各种资源,…

    Go语言 2023年5月25日
    049
  • go-zero单体服务使用泛型简化注册Handler路由

    一、Golang环境安装及配置Go Module https://go-zero.dev/cn/docs/prepare/golang-install mac OS安装Go 下载并…

    Go语言 2023年5月25日
    064
  • go微服务框架Kratos笔记(四)使用nacos作为远端配置中心

    初识nacos nacos是阿里开源的一款用于动态服务发现、配置管理和服务管理的平台。 官方介绍,Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特…

    Go语言 2023年5月25日
    069
  • Golang笔记

    本文主要为go的学习过程笔记。 一、基本介绍 1、开发环境安装-windows安装 打开Golang官网,选择对应版本,进行安装。 2、环境变量配置 1)步骤 (1)首先在环境变量…

    Go语言 2023年5月25日
    047
  • Go内存逃逸分析

    Go的内存逃逸及逃逸分析 Go的内存逃逸 分析内存逃逸之前要搞清楚一件事 我们编写的程序中的 函数和 局部变量默认是存放在栈上的(补充一点堆上存储的数据的指针 是存放在栈上的 因为…

    Go语言 2023年5月25日
    042
  • Go语言之网络编程

    一、网络编程基础 网络基础之TCP/IP协议族 网络编程之socket 二、TCP Socket编程 (一)流程 首先应该了解服务端和客户端的处理流程: 1、服务端处理流程 监听端…

    Go语言 2023年5月29日
    057
  • Go语言程序的命令行参数

    获取命令行参数是程序功能多样化的必要前提。 这个例子展示Go语言如何获得程序的命令行参数。 Go语言程序: // echoarg project main.go package m…

    Go语言 2023年5月29日
    046
  • go 连接MSSQLServer数据库【遇到的坑】

    前言:项目测试需要用到mssqlserver数据库连接,遇到坑,自己爬 直接上代码: go;gutter:true; package main</p> <p&gt…

    Go语言 2023年5月25日
    046
  • Go语言之数组与切片基础

    数组是同一类型元素的集合,可以放多个值,但是类型一致,内存中连续存储 Go 语言中不允许混合不同类型的元素,而且数组的大小,在定义阶段就确定了,不能更改 1、数组的定义 // 定义…

    Go语言 2023年5月25日
    063
  • 记一次提升18倍的性能优化

    背景 最近负责的一个自研的 Dubbo 注册中心经常收到 CPU 使用率的告警,于是进行了一波优化,效果还不错,于是打算分享下思考、优化过程,希望对大家有一些帮助。 自研 Dubb…

    Go语言 2023年5月25日
    070
  • go 错误处理设计思考

    前段时间准备对线上一个golang系统服务进行内部开源,对代码里面的错误处理进行了一波优化。 优化的几个原因: 错误处理信息随意,未分类未定义。看到错误日志不能第一时间定位 错误的…

    Go语言 2023年5月25日
    047
  • 开始读 Go 源码了

    学完 Go 的基础知识已经有一段时间了,那么接下来应该学什么呢?有几个方向可以考虑,比如说 Web 开发,网络编程等。 在写项目的过程中,发现一个问题。实现功能是没问题的,但不知道…

    Go语言 2023年5月25日
    040
  • Go – 如何编写 ProtoBuf 插件(二)?

    上篇文章《Go – 如何编写 ProtoBuf 插件 (一) 》,分享了使用 proto3 的 &#x81EA;&#x5B9A;&#x4E49;…

    Go语言 2023年5月25日
    052
  • Excelize 2.5.0 正式发布,这些新增功能值得关注

    Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,基于 ECMA-376,ISO/IEC 29500 国际标准。可以使用它来读取、写入由 Mic…

    Go语言 2023年5月25日
    058
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球