使用gopacket实现Prometheus对进程的流量监控和访问次数

前文prometheus client_java实现进程的CPU、内存、IO、流量的可观测是通过nethogs命令来监控进程的流量数据。本文使用gopacket实现进程的流量监控和访问次数。

gopacket是google出品的golang三方库,是个抓取网络数据包的库
抓包工具包括:

Windows平台下有Wireshark抓包工具,其底层抓包库是npcap(以前是winpcap);
Linux平台下有Tcpdump,其抓包库是libpcap;

gopacket库是libpcap和npcap的go封装,提供了更方便的go语言操作接口。
gopacket包含许多有用的附加功能的子包,包括:

layers:包含了内置在gopacket中用于解码数据包协议的逻辑。
pcap:使用libpcap从网络上读取数据包的C绑定。
pfring:使用PF_RING从网络上读取数据包的C绑定。
afpacket: Linux的AF_PACKET的C绑定,用于从网络上读取数据包。
tcppassemassembly: TCP流重组

官方地址:https://github.com/google/gopacket
API文档:https://godoc.org/github.com/google/gopacket
google官方bugfix慢,有个第三方分支版本
源码地址:https://github.com/gopacket/gopacket
文档地址:https://pkg.go.dev/github.com/gopacket/gopacket

1、环境准备

环境准备
安装第三方库 libpcap或npcap
windows 平台
如果是在windows平台下,需要确保安装了npcap或winpcap

npcap下载地址:https://nmap.org/npcap/

linux平台
如果实在linux平台下确保安装了libpcap库

# Linux (Debian/Ubuntu)
sudo apt-get update
sudo apt-get install libpcap-dev
# Linux (Fedora/RHEL/CentOS)
sudo dnf install libpcap-devel
# macOS 平台,使用 Homebrew 安装:
brew install libpcap

如果报错

# github.com/google/gopacket/pcap
/usr/local/go/go/pkg/mod/github.com/google/gopacket@v1.1.19/pcap/pcap_unix.go:34:10: fatal error: pcap.h: No such file or directory
34 | #include <pcap.h>
|          ^~~~~~~~
compilation terminated.

那就是没安装上面的第三方库

在Linux (Fedora/RHEL/CentOS)下可能会出现libpcap-devel没有安装的问题,可以通过如下命令安装:

# 安装
vim /etc/yum.repos.d/xxxxxx.repo
[PowerTools]
name=CentOS-$releasever - PowerTools - mirrors.aliyun.com
#failovermethod=priority
baseurl=http://mirrors.aliyun.com/centos/$releasever/PowerTools/$basearch/os/
        http://mirrors.aliyuncs.com/centos/$releasever/PowerTools/$basearch/os/
        http://mirrors.cloud.aliyuncs.com/centos/$releasever/PowerTools/$basearch/os/
gpgcheck=1
enabled=1
gpgkey=http://mirrors.aliyun.com/centos/RPM-GPG-KEY-CentOS-Official

# 执行如下命令安装
dnf config-manager --set-enabled PowerTools
yum install libpcap
yum install libpcap-devel

缺少gcc导致错误

# fixed by set CGO_ENABLED=1; and istall gcc with libcap-dev
dnf install gcc

2、实现抓包统计流量

实现原理:
1、在解析gopacket数据包时,从数据包中解析出ipLayer和tcpLayer两个层,从ipLayer获取源和目标的ip地址,从tcpLayer获取源和目的端口号。这样可以获取地址:源端口_目标地址:目标端口从而判断数据包是流入还是流出。
2、TCP包的标志位:CWR, ECE, URG, ACK, PSH, RST, SYN, FIN,
(1)当ACK、PSH为true时,表示数据包在传输,统计包的大小。
(2)当ACK、FIN为true时,表示数据包在传输完成,统计包的大小,结束当前统计。

# 标志位说明
CWR:#拥塞窗口减少标志被发送主机设置,用来表明它接收到了设置ECE标志的TCP包。拥塞窗口是被TCP维护的一个内部变量,用来管理发送窗口大小。
ECE:#ECE 响应标志被用来在TCP3次握手时表明一个TCP端是具备ECE功能的,并且表明接收到的TCP包的IP头部的ECN被设置为11。
URG:#此标志表示TCP包的紧急指针域(后面马上就要说到)有效,用来保证TCP连接不被中断,并且督促中间层设备要尽快处理这些数据。
ACK:#此标志表示应答域有效,就是说前面所说的TCP应答号将会包含在TCP数据包中;有两个取值:0和1,为1的时候表示应答域有效,反之为0。
PSH:#这个标志位表示Push操作。所谓Push操作就是指在数据包到达接收端以后,立即传送给应用程序,而不是在缓冲区中排队。
RST:#这个标志表示连接复位请求。用来复位那些产生错误的连接,也被用来拒绝错误和非法的数据包。
SYN:#表示同步序号,用来建立连接。SYN标志位和ACK标志位搭配使用,当连接请求的时候,SYN=1,ACK=0;连接被相应的时候,SYN=1,ACK=1;这个标志的数据包经常被用来进行端口扫描。扫描者发送一个只有SYN的数据包,如果对方主机响应了一个数据包回来 ,就表明这台主机存在这个端口;但是由于这种扫描方式只是进行TCP三次握手的第一次握手,因此这种扫描的成功表示被扫描的机器不很安全,一台安全的主机将会强制要求一个连接严格的进行TCP的三次握手。
FIN:#表示发送端已经达到数据末尾,也就是说双方的数据传送完成,没有数据可以传送了,发送FIN标志位的TCP数据包后,连接将被断开。这个标志的数据包也经常被用于进行端口扫描。当一个FIN标志的TCP数据包发送到一台计算机的特定端口,如果这台计算机响应了这个数据,并且反馈回来一个RST标志的TCP包,就表明这台计算机上没有打开这个端口,但是这台计算机是存在的;如果这台计算机没有反馈回来任何数据包,这就表明,这台被扫描的计算机存在这个端口。

3、对于http请求判断
(1)请求时http头信息,包括:GET /xxxx HTTP/1.1
(2)响应时包含:HTTP/1.1 200 OK
可以通过信息是否包含http协议来判断是否是http请求和响应完成。
4、判断当前在数据传输的进程,可以通过gopsutil实现,
(1)使用gopsutil获取服务器端监听的程序,通过数据交换的端口是否相同来判断是否是同一个进程。
(2)对于wget、curl之类工具进行上传或下载的,本地没有监听端口,但是有数据交换的端口,可以通过gopsutil的process.Processes()获取所有进程,然后遍历使用Process.Connections()遍历每个进程的链接,查找是否存在相同的端口来判断是否为相同进程。

主要的实现代码:

import (
	"bytes"
	"encoding/json"
	"fmt"
	"github.com/gopacket/gopacket"
	"github.com/gopacket/gopacket/layers"
	"github.com/gopacket/gopacket/pcap"
	"github.com/prometheus/client_golang/prometheus"
	"strconv"
	"strings"
	"sync"
	"time"
)
// 获取所有网卡的IP,需要用户有权限读取proc/pid目录
func findDeviceIp() {
	// 得到所有的(网络)设备
	devices, err := pcap.FindAllDevs()
	if err != nil {
		logger.Fatal(err)
		return
	}
	// 打印设备信息
	for _, device := range devices {
		var ip = ""
		for _, address := range device.Addresses {
			ip = address.IP.String()
			if common.IsMatch(ip, "[\\d\\.]+") {
				break
			}
		}
		if ip != "" {
			deviceIpMap[device.Name] = ip
		}
		logger.Info("===device Name: ", device.Name, ", ", ip)
	}
	jsonBytes, err := json.MarshalIndent(deviceIpMap2, "", "  ")
	logger.Info("======deviceIpMap: ", string(jsonBytes))
}
// 对指定的网卡进行监听,进行抓包,并把包放到队列通道中
func listenDevs() {
	//device = "eth0"
	networkDevice := common.ConfigInfo.NetworkDevice
	snapshot_len := int32(1024 * 1024)
	promiscuous := true
	//timeout := 2 * time.Second
	timeout := 200 * time.Millisecond
	devices := strings.Split(networkDevice, ",")
	for _, device := range devices {
		logger.Info("OpenLive======================", device)
		_, exists := deviceIpMap[device]
		if exists {
			go openLive(device, snapshot_len, promiscuous, timeout)
		} else {
			logger.Info("=======device not exists:", device)
		}
	}
}
func openLive(devi string, snaplen int32, promisc bool, timeout time.Duration) {
	handle, err := pcap.OpenLive(devi, snaplen, promisc, timeout)
	if err != nil {
		logger.Fatal(err)
	}
	defer handle.Close()

	// 设置过滤器 tcp port 8080 and tcp port 9311
	//if err := handle.SetBPFFilter("tcp and port 9311"); err != nil {
	//	logger.Info("set bpf filter failed: %v", err)
	//	return
	//}

	packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
	// Set up assembly

	for packet := range packetSource.Packets() {
		handlePacket(packet)
	}
}

func handlePacket(packet gopacket.Packet, chanCount uint64) {
	payload := ""
	// Let's see if the packet is IP (even though the ether type told us)
	// 判断数据包是否为IP数据包,可解析出源ip、目的ip、协议号等
	ipLayer4 := packet.Layer(layers.LayerTypeIPv4)
	if ipLayer4 == nil {
		return
	}

	ip, _ := ipLayer4.(*layers.IPv4)

	// 判断数据包是否为TCP数据包,可解析源端口、目的端口、seq序列号、tcp标志位等
	tcpLayer := packet.Layer(layers.LayerTypeTCP)
	if tcpLayer == nil {
		return
	}
	tcp, _ := tcpLayer.(*layers.TCP)

	payload = string(tcp.Payload)
	// 接收到数据包
	if tcp.ACK && tcp.PSH && len(tcp.Payload) > 0 {
		payload = strings.Split(payload, "\n")[0]
		dataDelivery(ip, tcp, packet)
	}
	// 传输结束
	if tcp.ACK && tcp.FIN {
		httpDelivery(ip, tcp, packet)
	}
	// 判断layer是否存在错误
	if err := packet.ErrorLayer(); err != nil {
		logger.Info(packet.Metadata().Timestamp, "========Error decoding some part of the packet:", err)
	}
}

// 统计数据包大小,传输流量
func dataDelivery(ip *layers.IPv4, tcp *layers.TCP, packet gopacket.Packet) {
	localIP := ip.SrcIP.String()
	localPort, _ := strconv.Atoi(tcp.TransportFlow().Src().String())
	remoteIP := ip.DstIP.String()
	remotePort, _ := strconv.Atoi(tcp.TransportFlow().Dst().String())
	byteCount := len(tcp.Payload)
	//logger.Info(packet.Metadata().Timestamp, ",handlePacket=====localIP:", localIP, ",localPort:", uint16(localPort), ",remoteIP:", remoteIP, ",remotePort:", uint16(remotePort), ",DstPort:", tcp.DstPort, ",byteCount:", byteCount) // , "Payload:", payload

	if byteCount > 0 {
		payloadPacket := models.PayloadPacket{localIP, localPort, remoteIP, remotePort, "TCP", byteCount}
		payloadChans[0] <- payloadPacket
	}
	// 获取http请求
	http, ok := httpMap.Get(ip.NetworkFlow().Reverse(), tcp.TransportFlow().Reverse())
	if ok {
		// 响应
		localIP, remoteIP = remoteIP, localIP
		localPort, remotePort = remotePort, localPort

		if http.response == nil {
			http.response = &statsStream{
				net:       ip.NetworkFlow(),
				transport: tcp.TransportFlow(),
				start:     &packet.Metadata().Timestamp,
				buffer:    bytes.Buffer{},
			}
		}
		http.response.bytes += int64(len(tcp.Payload))
	} else {
		// 请求
		http, ok = httpMap.Get(ip.NetworkFlow(), tcp.TransportFlow())
		if ok == false {
			s := &statsStream{
				net:       ip.NetworkFlow(),
				transport: tcp.TransportFlow(),
				start:     &packet.Metadata().Timestamp,
				buffer:    bytes.Buffer{},
			}
			http = &RequestResponse{nowTime: time.Now().UnixMilli()}
			http.request = s
		}
		//http.request.buffer.Write(tcp.Payload)
		http.request.bytes += int64(len(tcp.Payload))
		httpMap.Put(ip.NetworkFlow(), tcp.TransportFlow(), http)
	}
}

func httpDelivery(ip *layers.IPv4, tcp *layers.TCP, packet gopacket.Packet) {
	http, ok := httpMap.Get(ip.NetworkFlow().Reverse(), tcp.TransportFlow().Reverse())
	if ok {
		// 响应结束
		if http.response == nil {
			return
		}
		http.response.end = &packet.Metadata().Timestamp
		http.response.bytes += int64(len(tcp.Payload))
		http.response.sawEnd = true
	} else {
		// 请求结束
		http, ok = httpMap.Get(ip.NetworkFlow(), tcp.TransportFlow())
		if ok {
			if http.request == nil {
				return
			}
			http.request.end = &packet.Metadata().Timestamp
			http.request.bytes += int64(len(tcp.Payload))
			http.request.sawStart = true
		} else {
			return
		}
	}
	if http.request != nil && http.response != nil && http.request.sawStart && http.response.sawEnd {
		requestResponseChans <- *http
		httpMap.Remove(http.request.net, http.request.transport.Reverse())
	}
}

3、运行效果

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

penngo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值