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 是可以抓到经过网桥的转发流量的,我们使用 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 使用原始套接字捕获网卡流量的更多相关文章
- 《Python黑帽子:黑客与渗透测试编程之道》 网络:原始套接字和流量嗅探
Windows和Linux上的包嗅探: #!/usr/bin/python import socket import os #监听的主机 host = "10.10.10.160" ...
- Linux Socket 原始套接字编程
对于linux网络编程来说,可以简单的分为标准套接字编程和原始套接字编程,标准套接字主要就是应用层数据的传输,原始套接字则是可以获得不止是应用层的其他层不同协议的数据.与标准套接字相区别的主要是要开发 ...
- linux原始套接字(1)-arp请求与接收
一.概述 以太网的arp数据包结构: arp结构op操作参数:1为请求,2为应答. 常用的数据结构如 ...
- UNIX网络编程——原始套接字的魔力【续】
如何从链路层直接发送数据帧 上一篇里面提到的是从链路层"收发"数据,该篇是从链路层发送数据帧. 上一节我们主要研究了如何从链路层直接接收数据帧,可以通过bind函数来将原始套接字绑 ...
- UNIX网络编程——原始套接字的魔力【下】
可以接收链路层MAC帧的原始套接字 前面我们介绍过了通过原始套接字socket(AF_INET, SOCK_RAW, protocol)我们可以直接实现自行构造整个IP报文,然后对其收发.提醒一点,在 ...
- Go中原始套接字的深度实践
1. 介绍 2. 传输层socket 2.1 ICMP 2.2 TCP 2.3 传输层协议 3. 网络层socket 3.1 使用Go库 3.2 系统调用 3.3 网络层协议 4. 总结 4.1 参考 ...
- python使用原始套接字 解析原始ip头数据
使用底层套接字解码底层流量,是这次做的重点工作. 首先来捕获第一个包 # coding:utf-8import socket # 监听的主机IP host = "192.168.1.100& ...
- raw_socket(原始套接字)以及普通socket使用终极总结
一.传输层socket(四层socket,普通socket) 可参考本人以下博客: Windows Socket编程之UDP实现大文件的传输:http://blog.csdn.net/luchen ...
- 浅谈原始套接字 SOCK_RAW 的内幕及其应用(port scan, packet sniffer, syn flood, icmp flood)
一.SOCK_RAW 内幕 首先在讲SOCK_RAW 之前,先来看创建socket 的函数: int socket(int domain, int type, int protocol); domai ...
- Linux原始套接字实现分析---转
http://blog.chinaunix.net/uid-27074062-id-3388166.html 本文从IPV4协议栈原始套接字的分类入手,详细介绍了链路层和网络层原始套接字的特点及其内核 ...
随机推荐
- PyTorch的安装与使用
技术背景 PyTorch是一个非常常用的AI框架,主要归功于其简单易用的特点,深受广大科研人员的喜爱.在前面的一篇文章中我们介绍过制作PyTorch的Singularity镜像的方法,这里我们单独抽出 ...
- installshield 安装jdk并配置环境变量
今天来通过installshield安装jdk以及配置环境变量,本质上是调用第三方安装程序. 首先将jdk的安装文件添加到我们的安装程序中 然后编写我们的脚本 选择BEHAVIOR AND LOGIC ...
- PageOffice 在线编辑 office文件,回调父页面
一.子页面调用父页面的方法 var value=window.external.CallParentFunc("ParentFunName(Arguments);");//父页面的 ...
- AIRIOT物联网低代码平台如何配置Modbus TCP协议?
AIRIOT物联网低代码平台稳定性超高,支持上百种驱动,各种主流驱动已在大型项目中通过验证,持续稳定运行. AIRIOT物联网低代码平台如何配置Modbus TCP协议?操作如下: AIRIOT与西门 ...
- CeiT:商汤提出结合CNN优势的高效ViT模型 | 2021 arxiv
论文提出CeiT混合网络,结合了CNN在提取低维特征方面的局部性优势以及Transformer在建立长距离依赖关系方面的优势.CeiT在ImageNet和各种下游任务中达到了SOTA,收敛速度更快,而 ...
- 【题解】A18536.星光交错的律动
题目跳转 思路:这道题可能跟博弈论有一点关系,没有学习过博弈论做起来应该问题也不大.思考一个问题,先手必胜的前提是什么? 有关更多的内容可以前往:浅谈有向无环图 先手必胜的前提是,在任何一种局面下,先 ...
- C# dynamic动态对象赋值
dynamic 如果接收的是匿名对象,是无法为属性赋值的,而如果是接收的定义对象,又无法扩展字段. 解决办法序列化为json字符串,然后用Dictionary反序列化,就能赋值了.也能扩展新的字段. ...
- java jdbc 得到像C#里的datatable一样的表格
public ArrayList<ArrayList<String>> getDataTale2(String sql) throws SQLException { Array ...
- Java8 Lambda表达式入门
可能很多人都听说过java8的新特性----Lambada表达式,但可能很多人都不知道Lambda表达式到底有什么用,下面我带大家理解一下Lambada表达式. 在平时的编程中,我们常常会用到匿名内部 ...
- 在 Inno Setup iss 打包过程 中检测 .NET 6 / .net 5 / .NET Core 运行环境是否存在或已安装
为了将 .NET 5 / .NET Core 应用程序部署到客户机,我们可以编写 Inno Setup 代码来判断客户机是否安装了必要的运行环境..NET 官方仓库 中提供了一个名为 NetCoreC ...