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

参考

发表评论

评论已关闭。

相关文章