首页 > 其他分享 >Go 使用原始套接字捕获网卡流量

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

时间:2024-07-04 09:54:38浏览次数:15  
标签:return nil err syscall sock PACKET 网卡 Go 接字

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 和精简了解析逻辑,性能尚可能够满足要求。

参考

标签:return,nil,err,syscall,sock,PACKET,网卡,Go,接字
From: https://www.cnblogs.com/shuqin/p/18282991

相关文章

  • golang 打印类型和switch case选择
    因为需要打印传输信息,要用到pion的stats,而stats返回报告的又包含多个type,对于不同type有不同的数据统计,所以需要打印类型和switchcase针对于不同的type执行不同的命令。针对于返回变量类型,golang采用reflect包,对于变量valua,可以利用reflect.TypeOf(valua)返回变量类型,而这只能做......
  • 从零开始的Django+vue项目实战(1)
    Introduction这个系列的blog是为哈工大(威海)的企业与服务智能计算研究中心(ICES)的纳新培训任务准备的,但是也适合想学习django并快速上手项目的友友。我们培训的目的要使新人学会web前后端开发。培训PPT里给出了用springboot和vue3来实现,但是如今django也愈发流行,为了弥补P......
  • bet9链接成功用Google Drive 开机!
    本文由bet9链接:вт989点сс编译原创,最近传出国外有一名电脑科学系的学生SambhavS成功从Google云端储存平台启动Linux系统,打造出「云端原生电脑」。这项创举源于他想超越朋友实现的「从网路档案系统(NFS)开机」成就。一般来说,电脑开机需要从内建硬碟读取作业系统和其他必......
  • Django学习第二天
    启动项目命令pythonmanage.pyrunserver动态获取当前时间javascript实现数据动态更新代码<script>setInterval(function(){varcurrentTimeElement=document.getElementById('current-time');varcurrentTime=newDate();......
  • Google Earth Engine(GEE)——ui.Select的使用和分析
    结果 函数ui.Select(items, placeholder, value, onChange, disabled, style)Aprintableselectmenuwithacallback.Arguments:items(List<Object>,optional):Thelistofoptionstoaddtotheselect.Defaultstoanemptyarray.placeholder(String......
  • Go 如何对多个网络命令空间中的端口进行监听
    Go如何对多个网络命令空间中的端口进行监听需求为对多个命名空间内的端口进行监听和代理。刚开始对netns的理解不够深刻,以为必须存在一个新的线程然后调用setns(2)切换过去,如果有新的netns那么需要再新建一个线程切换过去使用,这样带来的问题就是线程数量和netns的数量......
  • django西餐厅管理系统-计算机毕业设计源码10873
    摘要在现代餐饮行业中,高效的管理系统对于西餐厅的成功运营至关重要。为了满足西餐厅日益增长的管理需求,设计并实现了一款基于Python的西餐厅管理系统。Python作为一种简洁而易读的编程语言,具有广泛的应用领域,包括Web开发。结合Django这一强大的Web框架,我们可以快速构建......
  • Go设置GOPROXY国内加速
    在Linux或macOS上面需要运行下面命令(或者,可以把以下命令写到.bashrc或.bash_profile文件中):#启用GoModules功能goenv-wGO111MODULE=on#配置GOPROXY环境变量,以下三选一#1.七牛CDNgoenv-wGOPROXY=https://goproxy.cn,direct#2.阿里云goenv......
  • Go标准库:container/list
    Go标准库:container/list原创 孟斯特 孟斯特 2024-07-0316:03 北京 听全文在Go语言的标准库中,container/list包提供了一个双向链表的实现,这对于需要频繁插入和删除操作的场景非常有用。双向链表是一种线性数据结构,它由一系列节点组成,每个节点包含数据和两个指针,分别......
  • go当中的线程存储与使用
    1、settls从引导代码中可以看到,在执行settls前将m.tls放入了DI。go/src/runtime/asm_amd64.s:159TEXTruntime·rt0_go(SB),NOSPLIT|NOFRAME|TOPFRAME,$0....LEAQruntime·m0+m_tls(SB),DICALLruntime·settls(SB)....tls定义是无符号整形指针ty......