设为首页收藏本站
网站公告 | 这是第一条公告
     

 找回密码
 立即注册
缓存时间12 现在时间12 缓存数据 最令人心酸的是,没删除 不聊天 没交集

最令人心酸的是,没删除 不聊天 没交集 -- 爱江山更爱美人

查看: 913|回复: 2

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

[复制链接]

  离线 

TA的专栏

  • 打卡等级:即来则安
  • 打卡总天数:15
  • 打卡月天数:0
  • 打卡总奖励:241
  • 最近打卡:2023-08-27 06:18:17
等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
39
主题
33
精华
0
金钱
362
积分
80
注册时间
2023-8-13
最后登录
2025-5-31

发表于 2024-7-5 09:14:50 | 显示全部楼层 |阅读模式
目录


  • 捕获经过网桥的非本机流量
  • 捕获 VLAN 流量
  • 完整的 OpenRawSocket 实现
  • 从 fd 中读取数据
  • 总结
  • 参考
Go 捕获网卡流量使用最多的库为 github.com/google/gopacket,需要依赖 libpcap 导致必须开启 CGO 才能够进行编译。
为了减少对环境的依赖可以使用原始套接字捕获网卡流量,然后使用 gopacket 的协议解析功能,这样就省去了解析这部分的工作量,正确性也可以得到保证,同时 CGO 也可以关闭。
cilium 里有一个原始套接字打开的测试用例:
  1. // Both openRawSock and htons are available in
  2. // https://github.com/cilium/ebpf/blob/master/example_sock_elf_test.go.
  3. // MIT license.
  4. func OpenRawSocket(index int) (int, error) {
  5.         sock, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW|syscall.SOCK_NONBLOCK|syscall.SOCK_CLOEXEC, int(htons(syscall.ETH_P_ALL)))
  6.         if err != nil {
  7.                 return 0, err
  8.         }
  9.         sll := syscall.SockaddrLinklayer{Ifindex: index, Protocol: htons(syscall.ETH_P_ALL)}
  10.         if err := syscall.Bind(sock, &sll); err != nil {
  11.                 syscall.Close(sock)
  12.                 return 0, err
  13.         }
  14.         return sock, nil
  15. }
  16. // htons converts the unsigned short integer hostshort from host byte order to network byte order.
  17. func htons(i uint16) uint16 {
  18.         b := make([]byte, 2)
  19.         binary.BigEndian.PutUint16(b, i)
  20.         return *(*uint16)(unsafe.Pointer(&b[0]))
  21. }
复制代码
但是这个示例有一个问题,只能拿到本机流量。

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

通过 tcpdump 是可以抓到经过网桥的转发流量的,我们使用 stracetcpdump 进行跟踪分析
  1. root@localhost:~# strace -f tcpdump -i b_2_0 arp -nne
  2. ...
  3. socket(AF_PACKET, SOCK_RAW, htons(0 /* ETH_P_??? */)) = 4
  4. ioctl(4, SIOCGIFINDEX, {ifr_name="lo", ifr_ifindex=1}) = 0
  5. ioctl(4, SIOCGIFHWADDR, {ifr_name="b_2_0", ifr_hwaddr={sa_family=ARPHRD_ETHER, sa_data=4e:59:d6:32:f6:42}}) = 0
  6. newfstatat(AT_FDCWD, "/sys/class/net/b_2_0/wireless", 0x7ffdf063bc50, 0) = -1 ENOENT (No such file or directory)
  7. openat(AT_FDCWD, "/sys/class/net/b_2_0/dsa/tagging", O_RDONLY) = -1 ENOENT (No such file or directory)
  8. ioctl(4, SIOCGIFINDEX, {ifr_name="b_2_0", ifr_ifindex=6053}) = 0
  9. 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
  10. getsockopt(4, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
  11. 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
  12. getsockopt(4, SOL_SOCKET, SO_BPF_EXTENSIONS, [64], [4]) = 0
  13. mmap(NULL, 266240, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fec47cbe000
复制代码
看到有一个 setsockopt(PACKET_MR_PROMISC) 设置,看起来是开启的混杂模式,查看资料看到这是一个针对套接字级别的混杂模式。
由于之前看过 suricata 的代码,看看它是怎么做的,直接在 suricata 的仓库里面搜索 PACKET_MR_PROMISC 关键字,出现代码
  1. memset(&sock_params, 0, sizeof(sock_params));
  2. sock_params.mr_type = PACKET_MR_PROMISC;
  3. sock_params.mr_ifindex = bind_address.sll_ifindex;
  4. r = setsockopt(ptv->socket, SOL_PACKET, PACKET_ADD_MEMBERSHIP,(void *)&sock_params, sizeof(sock_params));
  5. if (r < 0) {
  6.     SCLogError("%s: failed to set promisc mode: %s", devname, strerror(errno));
  7.     goto socket_err;
  8. }
复制代码
套接字设置混杂模式的 Go 实现如下
  1. // Set socket level PROMISC mode
  2. err = unix.SetsockoptPacketMreq(sock, syscall.SOL_PACKET, syscall.PACKET_ADD_MEMBERSHIP, &unix.PacketMreq{Type: unix.PACKET_MR_PROMISC, Ifindex: int32(index)})
  3. if err != nil {
  4.         syscall.Close(sock)
  5.         return 0, err
  6. }
复制代码
捕获 VLAN 流量

目前只能拿到普通的以太网流量,如果还需要拿到 VLAN Id 的话,需要设置 PACKET_AUXDATA,参考 man packet
  1. PACKET_AUXDATA (since Linux 2.6.21)
  2.     If this binary option is enabled, the packet socket passes
  3.     a metadata structure along with each packet in the
  4.     recvmsg(2) control field.  The structure can be read with
  5.     cmsg(3).  It is defined as
  6.        struct tpacket_auxdata {
  7.            __u32 tp_status;
  8.            __u32 tp_len;      /* packet length */
  9.            __u32 tp_snaplen;  /* captured length */
  10.            __u16 tp_mac;
  11.            __u16 tp_net;
  12.            __u16 tp_vlan_tci;
  13.            __u16 tp_vlan_tpid; /* Since Linux 3.14; earlier, these
  14.                                   were unused padding bytes */
  15.        };
复制代码
Go 的实现如下
  1. // Enable PACKET_AUXDATA option for VLAN
  2. if err := syscall.SetsockoptInt(sock, syscall.SOL_PACKET, unix.PACKET_AUXDATA, 1); err != nil {
  3.         syscall.Close(sock)
  4.         return 0, err
  5. }
复制代码
完整的 OpenRawSocket 实现

完整的实现如下
  1. func OpenRawSocket(index int) (int, error) {
  2.         sock, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW|syscall.SOCK_NONBLOCK|syscall.SOCK_CLOEXEC, int(htons(syscall.ETH_P_ALL)))
  3.         if err != nil {
  4.                 return 0, err
  5.         }
  6.         // Enable PACKET_AUXDATA option for VLAN
  7.         if err := syscall.SetsockoptInt(sock, syscall.SOL_PACKET, unix.PACKET_AUXDATA, 1); err != nil {
  8.                 syscall.Close(sock)
  9.                 return 0, err
  10.         }
  11.         // Set socket level PROMISC mode
  12.         err = unix.SetsockoptPacketMreq(sock, syscall.SOL_PACKET, syscall.PACKET_ADD_MEMBERSHIP, &unix.PacketMreq{Type: unix.PACKET_MR_PROMISC, Ifindex: int32(index)})
  13.         if err != nil {
  14.                 syscall.Close(sock)
  15.                 return 0, err
  16.         }
  17.         sll := syscall.SockaddrLinklayer{Ifindex: index, Protocol: htons(syscall.ETH_P_ALL)}
  18.         if err := syscall.Bind(sock, &sll); err != nil {
  19.                 syscall.Close(sock)
  20.                 return 0, err
  21.         }
  22.         return sock, nil
  23. }
复制代码
从 fd 中读取数据

这里使用 select(2) 简单地对 fd 进行监听,使用 recvmsg(2) 来读取数据,包括 VLAN tag。
实现如下
  1. package pcap
  2. import (
  3.         "context"
  4.         "syscall"
  5. )
  6. func FD_SET(fd int, p *syscall.FdSet) {        p.Bits[fd/64] |= 1 << (uint(fd) % 64) }
  7. func FD_CLR(fd int, p *syscall.FdSet) {        p.Bits[fd/64] &^= 1 << (uint(fd) % 64) }
  8. func FD_ISSET(fd int, p *syscall.FdSet) bool {        return p.Bits[fd/64]&(1<<(uint(fd)%64)) != 0 }
  9. func FD_ZERO(p *syscall.FdSet) {
  10.         for i := range p.Bits {
  11.                 p.Bits[i] = 0
  12.         }
  13. }
  14. type RecvmsgHandler func(buf []byte, n int, oob []byte, oobn int, err error) error
  15. func RecvmsgLoop(ctx context.Context, sockfd int, fn RecvmsgHandler) error {
  16.         buf := make([]byte, 1024*64)
  17.         oob := make([]byte, syscall.CmsgSpace(1024))
  18.         readfds := syscall.FdSet{}
  19.         for {
  20.                 select {
  21.                 case <-ctx.Done():
  22.                         return ctx.Err()
  23.                 default:
  24.                 }
  25.                 FD_ZERO(&readfds)
  26.                 FD_SET(sockfd, &readfds)
  27.                 tv := syscall.Timeval{Sec: 0, Usec: 100000} // 100ms
  28.                 nfds, err := syscall.Select(sockfd+1, &readfds, nil, nil, &tv)
  29.                 if err != nil {
  30.                         continue
  31.                 }
  32.                 if nfds > 0 && FD_ISSET(sockfd, &readfds) {
  33.                         n, oobn, _, _, err := syscall.Recvmsg(sockfd, buf, oob, 0)
  34.                         err = fn(buf, n, oob, oobn, err)
  35.                         if err != nil {
  36.                                 return err
  37.                         }
  38.                 }
  39.         }
  40. }
复制代码
VLAN 数据的解析逻辑如下
  1. func decodeVlanIdByAuxData(oob []byte) (uint16, error) {
  2.         msgs, err := syscall.ParseSocketControlMessage(oob)
  3.         if err != nil {
  4.                 return 0, err
  5.         }
  6.         for _, m := range msgs {
  7.                 if m.Header.Level == syscall.SOL_PACKET && m.Header.Type == 8 && len(m.Data) >= 20 {
  8.                         auxdata := unix.TpacketAuxdata{
  9.                                 Status:   binary.LittleEndian.Uint32(m.Data[0:4]),
  10.                                 Vlan_tci: binary.LittleEndian.Uint16(m.Data[16:18]),
  11.                         }
  12.                         if auxdata.Status&unix.TP_STATUS_VLAN_VALID != 0 {
  13.                                 return auxdata.Vlan_tci, nil
  14.                         }
  15.                 }
  16.         }
  17.         return 0, nil
  18. }
复制代码
总结

以上代码都在实际的场景中使用,只是稍微修改了一点细节以及使用 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原始套接字内容请搜索晓枫资讯以前的文章或继续浏览下面的相关文章希望大家以后多多支持晓枫资讯!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
晓枫资讯-科技资讯社区-免责声明
免责声明:以上内容为本网站转自其它媒体,相关信息仅为传递更多信息之目的,不代表本网观点,亦不代表本网站赞同其观点或证实其内容的真实性。
      1、注册用户在本社区发表、转载的任何作品仅代表其个人观点,不代表本社区认同其观点。
      2、管理员及版主有权在不事先通知或不经作者准许的情况下删除其在本社区所发表的文章。
      3、本社区的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,举报反馈:点击这里给我发消息进行删除处理。
      4、本社区一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
      5、以上声明内容的最终解释权归《晓枫资讯-科技资讯社区》所有。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
22
积分
24
注册时间
2022-12-26
最后登录
2022-12-26

发表于 2025-2-20 09:56:01 | 显示全部楼层
感谢楼主,顶。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
16
积分
12
注册时间
2022-12-25
最后登录
2022-12-25

发表于 2025-4-7 11:53:56 | 显示全部楼层
路过,支持一下
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~
严禁发布广告,淫秽、色情、赌博、暴力、凶杀、恐怖、间谍及其他违反国家法律法规的内容。!晓枫资讯-社区
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

1楼
2楼
3楼

手机版|晓枫资讯--科技资讯社区 本站已运行

CopyRight © 2022-2025 晓枫资讯--科技资讯社区 ( BBS.yzwlo.com ) . All Rights Reserved .

晓枫资讯--科技资讯社区

本站内容由用户自主分享和转载自互联网,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。

如有侵权、违反国家法律政策行为,请联系我们,我们会第一时间及时清除和处理! 举报反馈邮箱:点击这里给我发消息

Powered by Discuz! X3.5

快速回复 返回顶部 返回列表