Go 使用原始套接字捕获网卡流量

go · 浏览次数 : 31

小编点评

本文介绍了一种在 Go 语言中捕获网卡流量的方法,避免使用原始套接字和 libpcap 库,以减少环境依赖并提高正确性。文章首先指出使用 gopacket 库捕获网卡流量需要开启 CGO,然后提出了一种不依赖 libpcap 的方法,通过原始套接字和 gopacket 协议解析功能来实现网卡流量的捕获。最后,文章提供了一个具体的示例,展示了如何使用原始套接字捕获网卡流量,并解析出 VLAN 流量。 1. **背景与挑战**: 本文首先介绍了在 Go 语言中捕获网卡流量的挑战,指出了使用原始套接字和 libpcap 库所需的 CGO 开启条件,以及这种方式带来的环境依赖问题。 2. **解决方案**: 提出了使用 gopacket 库的方法,该库提供了处理网络数据包的功能,但无需依赖 libpcap。文章还提到了如何通过设置套接字级别的全局混杂模式来捕获所有网络流量。 3. **实现细节**: 详细阐述了如何使用原始套接字捕获流量,包括创建套接字、绑定地址、设置混杂模式等步骤。同时,解释了如何处理 VLAN 流量,包括如何从辅助数据中提取 VLAN ID。 4. **性能优化**: 文章还讨论了如何通过 epoll(2) 监听文件描述符来提高性能,并提到了使用 sync.Pool 来重用内存,从而减少内存分配和释放的开销。 5. **参考资料**: 最后,文章提供了一些参考资料,包括 suricata 的源代码和 man 页面,以及相关的文档,以便读者进一步学习和参考。 总结: 本文提出了一种在 Go 语言中捕获网卡流量的方法,通过使用原始套接字和 gopacket 库,避免了依赖 libpcap 库和开启 CGO。文章还详细讨论了实现细节,包括创建套接字、绑定地址、设置混杂模式、处理 VLAN 流量等,并提供了一些性能优化策略和参考资料。

正文

Go 使用原始套接字捕获网卡流量

Go 捕获网卡流量使用最多的库为 github.com/google/gopacket,需要依赖 libpcap 导致必须开启 CGO 才能够进行编译。

为了减少对环境的依赖可以使用原始套接字捕获网卡流量,然后使用 gopacket 的协议解析功能,这样就省去了解析这部分的工作量,正确性也可以得到保证,同时 CGO 也可以关闭。

cilium 里有一个原始套接字打开的测试用例:

// Both openRawSock and htons are available in
// https://github.com/cilium/ebpf/blob/master/example_sock_elf_test.go.
// MIT license.

func OpenRawSocket(index int) (int, error) {
	sock, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW|syscall.SOCK_NONBLOCK|syscall.SOCK_CLOEXEC, int(htons(syscall.ETH_P_ALL)))
	if err != nil {
		return 0, err
	}

	sll := syscall.SockaddrLinklayer{Ifindex: index, Protocol: htons(syscall.ETH_P_ALL)}
	if err := syscall.Bind(sock, &sll); err != nil {
		syscall.Close(sock)
		return 0, err
	}
	return sock, nil
}

// htons converts the unsigned short integer hostshort from host byte order to network byte order.
func htons(i uint16) uint16 {
	b := make([]byte, 2)
	binary.BigEndian.PutUint16(b, i)
	return *(*uint16)(unsafe.Pointer(&b[0]))
}

但是这个示例有一个问题,只能拿到本机流量。

捕获经过网桥的非本机流量

通过 tcpdump 是可以抓到经过网桥的转发流量的,我们使用 stracetcpdump 进行跟踪分析

root@localhost:~# strace -f tcpdump -i b_2_0 arp -nne
...
socket(AF_PACKET, SOCK_RAW, htons(0 /* ETH_P_??? */)) = 4
ioctl(4, SIOCGIFINDEX, {ifr_name="lo", ifr_ifindex=1}) = 0
ioctl(4, SIOCGIFHWADDR, {ifr_name="b_2_0", ifr_hwaddr={sa_family=ARPHRD_ETHER, sa_data=4e:59:d6:32:f6:42}}) = 0
newfstatat(AT_FDCWD, "/sys/class/net/b_2_0/wireless", 0x7ffdf063bc50, 0) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/sys/class/net/b_2_0/dsa/tagging", O_RDONLY) = -1 ENOENT (No such file or directory)
ioctl(4, SIOCGIFINDEX, {ifr_name="b_2_0", ifr_ifindex=6053}) = 0
bind(4, {sa_family=AF_PACKET, sll_protocol=htons(0 /* ETH_P_??? */), sll_ifindex=if_nametoindex("b_2_0"), sll_hatype=ARPHRD_NETROM, sll_pkttype=PACKET_HOST, sll_halen=0}, 20) = 0
getsockopt(4, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
setsockopt(4, SOL_PACKET, PACKET_ADD_MEMBERSHIP, {mr_ifindex=if_nametoindex("b_2_0"), mr_type=PACKET_MR_PROMISC, mr_alen=0, mr_address=4e:59:d6:32:f6:42}, 16) = 0
getsockopt(4, SOL_SOCKET, SO_BPF_EXTENSIONS, [64], [4]) = 0
mmap(NULL, 266240, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fec47cbe000

看到有一个 setsockopt(PACKET_MR_PROMISC) 设置,看起来是开启的混杂模式,查看资料看到这是一个针对套接字级别的混杂模式。

由于之前看过 suricata 的代码,看看它是怎么做的,直接在 suricata 的仓库里面搜索 PACKET_MR_PROMISC 关键字,出现代码

memset(&sock_params, 0, sizeof(sock_params));
sock_params.mr_type = PACKET_MR_PROMISC;
sock_params.mr_ifindex = bind_address.sll_ifindex;
r = setsockopt(ptv->socket, SOL_PACKET, PACKET_ADD_MEMBERSHIP,(void *)&sock_params, sizeof(sock_params));
if (r < 0) {
    SCLogError("%s: failed to set promisc mode: %s", devname, strerror(errno));
    goto socket_err;
}

套接字设置混杂模式的 Go 实现如下

// Set socket level PROMISC mode
err = unix.SetsockoptPacketMreq(sock, syscall.SOL_PACKET, syscall.PACKET_ADD_MEMBERSHIP, &unix.PacketMreq{Type: unix.PACKET_MR_PROMISC, Ifindex: int32(index)})
if err != nil {
	syscall.Close(sock)
	return 0, err
}

捕获 VLAN 流量

目前只能拿到普通的以太网流量,如果还需要拿到 VLAN Id 的话,需要设置 PACKET_AUXDATA,参考 man packet

PACKET_AUXDATA (since Linux 2.6.21)
    If this binary option is enabled, the packet socket passes
    a metadata structure along with each packet in the
    recvmsg(2) control field.  The structure can be read with
    cmsg(3).  It is defined as

       struct tpacket_auxdata {
           __u32 tp_status;
           __u32 tp_len;      /* packet length */
           __u32 tp_snaplen;  /* captured length */
           __u16 tp_mac;
           __u16 tp_net;
           __u16 tp_vlan_tci;
           __u16 tp_vlan_tpid; /* Since Linux 3.14; earlier, these
                                  were unused padding bytes */
       };

Go 的实现如下

// Enable PACKET_AUXDATA option for VLAN
if err := syscall.SetsockoptInt(sock, syscall.SOL_PACKET, unix.PACKET_AUXDATA, 1); err != nil {
	syscall.Close(sock)
	return 0, err
}

完整的 OpenRawSocket 实现

完整的实现如下

func OpenRawSocket(index int) (int, error) {
	sock, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW|syscall.SOCK_NONBLOCK|syscall.SOCK_CLOEXEC, int(htons(syscall.ETH_P_ALL)))
	if err != nil {
		return 0, err
	}
	// Enable PACKET_AUXDATA option for VLAN
	if err := syscall.SetsockoptInt(sock, syscall.SOL_PACKET, unix.PACKET_AUXDATA, 1); err != nil {
		syscall.Close(sock)
		return 0, err
	}
	// Set socket level PROMISC mode
	err = unix.SetsockoptPacketMreq(sock, syscall.SOL_PACKET, syscall.PACKET_ADD_MEMBERSHIP, &unix.PacketMreq{Type: unix.PACKET_MR_PROMISC, Ifindex: int32(index)})
	if err != nil {
		syscall.Close(sock)
		return 0, err
	}

	sll := syscall.SockaddrLinklayer{Ifindex: index, Protocol: htons(syscall.ETH_P_ALL)}
	if err := syscall.Bind(sock, &sll); err != nil {
		syscall.Close(sock)
		return 0, err
	}
	return sock, nil
}

从 fd 中读取数据

这里使用 select(2) 简单地对 fd 进行监听,使用 recvmsg(2) 来读取数据,包括 VLAN tag。

实现如下

package pcap

import (
	"context"
	"syscall"
)

func FD_SET(fd int, p *syscall.FdSet) {	p.Bits[fd/64] |= 1 << (uint(fd) % 64) }
func FD_CLR(fd int, p *syscall.FdSet) {	p.Bits[fd/64] &^= 1 << (uint(fd) % 64) }
func FD_ISSET(fd int, p *syscall.FdSet) bool {	return p.Bits[fd/64]&(1<<(uint(fd)%64)) != 0 }
func FD_ZERO(p *syscall.FdSet) {
	for i := range p.Bits {
		p.Bits[i] = 0
	}
}

type RecvmsgHandler func(buf []byte, n int, oob []byte, oobn int, err error) error

func RecvmsgLoop(ctx context.Context, sockfd int, fn RecvmsgHandler) error {
	buf := make([]byte, 1024*64)
	oob := make([]byte, syscall.CmsgSpace(1024))
	readfds := syscall.FdSet{}

	for {
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
		}

		FD_ZERO(&readfds)
		FD_SET(sockfd, &readfds)
		tv := syscall.Timeval{Sec: 0, Usec: 100000} // 100ms

		nfds, err := syscall.Select(sockfd+1, &readfds, nil, nil, &tv)
		if err != nil {
			continue
		}

		if nfds > 0 && FD_ISSET(sockfd, &readfds) {
			n, oobn, _, _, err := syscall.Recvmsg(sockfd, buf, oob, 0)
			err = fn(buf, n, oob, oobn, err)
			if err != nil {
				return err
			}
		}
	}
}

VLAN 数据的解析逻辑如下

func decodeVlanIdByAuxData(oob []byte) (uint16, error) {
	msgs, err := syscall.ParseSocketControlMessage(oob)
	if err != nil {
		return 0, err
	}

	for _, m := range msgs {
		if m.Header.Level == syscall.SOL_PACKET && m.Header.Type == 8 && len(m.Data) >= 20 {
			auxdata := unix.TpacketAuxdata{
				Status:   binary.LittleEndian.Uint32(m.Data[0:4]),
				Vlan_tci: binary.LittleEndian.Uint16(m.Data[16:18]),
			}
			if auxdata.Status&unix.TP_STATUS_VLAN_VALID != 0 {
				return auxdata.Vlan_tci, nil
			}
		}
	}
	return 0, nil
}

总结

以上代码都在实际的场景中使用,只是稍微修改了一点细节以及使用 epoll(2) 来监听,结合 sync.Pool 和精简了解析逻辑,性能尚可能够满足要求。

参考

与Go 使用原始套接字捕获网卡流量相似的内容:

Go 使用原始套接字捕获网卡流量

Go 使用原始套接字捕获网卡流量 Go 捕获网卡流量使用最多的库为 github.com/google/gopacket,需要依赖 libpcap 导致必须开启 CGO 才能够进行编译。 为了减少对环境的依赖可以使用原始套接字捕获网卡流量,然后使用 gopacket 的协议解析功能,这样就省去了解析

AI来实现代码转换!Python转Java,Java转Go不在话下?

今天看到个有趣的网站,给大家分享一下。 该网站的功能很神奇,可以实现编程语言的转化。感觉在一些场景之下还是有点作用的,比如你原来跟我一样是做Java的,因为工作需要突然转Go。这个时候用你Java的经验 + 这个工具,或许可以起到一定的帮助作用。 工具的使用也很简单,只需要在左侧黏贴你想转换的原始代

golang pprof 监控系列(1) —— go trace 统计原理与使用

golang pprof 监控系列(1) —— go trace 统计原理与使用 服务监控系列文章 服务监控系列视频 关于go tool trace的使用,网上有相当多的资料,但拿我之前初学golang的经验来讲,很多资料都没有把go tool trace中的相关指标究竟是统计的哪些方法,统计了哪段

Go with Protobuf

原文在这里。 本教程为 Go 程序员提供了使用Protocol buffer的基本介绍。 本教程使用proto3向 Go 程序员介绍如何使用 protobuf。通过创建一个简单的示例应用程序,它向你展示了如何: 在.proto中定义消息格式 使用protocol buffer编译器 使用Go pro

[转帖]eBPF系列学习(4)了解libbpf、CO-RE (Compile Once – Run Everywhe) | 使用go开发ebpf程序(云原生利器cilium ebpf )

文章目录 一、了解libbpf1. BPF的可移植性CO-RE (Compile Once – Run Everywhere)BPF 可移植性面临的问题BPF的可移植性CO-RE (Compile Once – Run Everywhere) 2. libbpf和bcc性能对比3. 了解libbpf

gRPC基本教程

原文在[这里](https://grpc.io/docs/languages/go/basics/)。 本教程为Go程序员提供了使用gRPC的基本介绍。 通过跟随本示例,你将学会如何: - 在.proto文件中定义一个服务。 - 使用协议缓冲编译器生成服务器和客户端代码。 - 使用Go gRPC A

golang trace view 视图详解

> 大家好,我是蓝胖子,在golang中可以使用go pprof的工具对golang程序进行性能分析,其中通过go trace 命令生成的trace view视图对于我们分析系统延迟十分有帮助,鉴于当前对trace view视图的介绍还是很少,在粗略的看过trace统计原理后,我将对这部分做比较详细

go项目实现mysql接入以及web api

本文为博主原创,转载请注明出处: 创建go项目,并在go项目中接入mysql,将mysql的配置项单独整理放到一个胚子和文件中,支持项目启动时,通过加载配置文件中的值,然后创建数据库连接。 之后使用net/http相关的库,创建路由,并在路由中通过不同的http方法,实现mysql连接的test数据

go高并发之路——go语言如何解决并发问题

一、选择GO的原因 作为一个后端开发,日常工作中接触最多的两门语言就是PHP和GO了。无可否认,PHP确实是最好的语言(手动狗头哈哈),写起来真的很舒爽,没有任何心智负担,字符串和整型压根就不用区分,开发速度真的是比GO快很多。现在工作中也还是有一些老项目在使用PHP,但21年之后的新项目基本上就都

Go函数介绍与一等公民

Go函数介绍与一等公民 函数对应的英文单词是 Function,Function 这个单词原本是功能、职责的意思。编程语言使用 Function 这个单词,表示将一个大问题分解后而形成的、若干具有特定功能或职责的小任务,可以说十分贴切。函数代表的小任务可以在一个程序中被多次使用,甚至可以在不同程序中