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 是可以抓到经过网桥的转发流量的,我们使用 strace 对 tcpdump 进行跟踪分析
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 和精简了解析逻辑,性能尚可能够满足要求。
参考
- https://github.com/oisf/suricata/blob/ce727cf4b1ccbac1679272f14cbfa529bc23ebc6/src/source-af-packet.c#l1926,suricata 捕获网卡流量的实现
- https://man7.org/linux/man-pages/man7/packet.7.html, packet 文档
到此这篇关于go 使用原始套接字捕获网卡流量的文章就介绍到这了,更多相关go原始套接字内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论