一 概述
上述篇章中讲述的只是原始的抓包流程。
原始的抓包流程?简单的说就是创建socket,设置bpf后,每次接收数据包都要调用recvfrom系统调用。而每次调用recvfrom内核底层抓到的数据包都需要用内核copy到用户。不管是系统调用,还是copy都是相当耗cpu性能的。而linux内核提供了一种更高效的抓包方式packet_mmap.
packet_mmap是什么呢?它又是如何比原始的抓包流程高效的?如果你对mmap了解,其实就是PACKET_MMAP在内核空间中分配一块内核缓冲区,然后用户空间程序调用mmap映射到用户空间。将接收到的skb拷贝到那块内核缓冲区中,这样用户空间的程序就可以直接读到捕获的数据包了。从而减少了数据从内核copy到用户的性能消耗。
PACKET_MMAP提供一个映射到用户空间的大小可配置的环形缓冲区,读取报文只需要等待报文就可以了(poll 其实也是系统调用),当内核抓到数据包放入环形缓存时,poll就会知道,同时用户层可以根据状态(TP_STATUS_USER)来判断环形缓冲区哪些可以用户层处理的数据包。接收处理后再将状态设置为TP_STATUS_KERNEL。告诉内核缓存区中这块数据用户处理完了,内核可以自己处理。内核处理完后又会将状态设置为TP_STATUS_USER。
官方文档packet_mmap.txt中介绍了使用的过程,本文借鉴文档里的内容。
需特别注意:本博客主要讲解抓包packet_mmap的实现,不涉及发包。发包会在后一篇博客单独说明
[setup]
· socket() ------> 捕获socket的创建
· setsockopt() ------> 设置接收环形缓冲区 PACKET_RX_RING
· mmap() ------> 将分配的缓冲区映射到用户空间中
[capture]
· poll() ------> 等待底层有捕获到新的数据包
[shutdown]
· close ------> 关闭socket资源
主要看setup中几个关键步骤
1. 创建 socket
int fd = socket(PF_PACKET, mode, htons(ETH_P_ALL));
· 1
mode的设置主要有两种
· SOCK_RAW,链路层信息也会被捕获;
· SOCK_DGRAM,抓到的数据包将去除链路层的信息
2. 设置环形缓冲区
struct tpacket_req {
unsigned int tp_block_size; /* Minimal size of contiguous block */
unsigned int tp_block_nr; /* Number of blocks */
unsigned int tp_frame_size; /* Size of frame */
unsigned int tp_frame_nr; /* Total number of frames */
};
setsockopt(fd, SOL_PACKET, PACKET_RX_RING, (void *)&req, sizeof(req));
捕获frame被划分为多个block,每个block是一块物理上连续的内存区域,每个block有tp_block_size/tp_frame_size 个frame。也就是说tp_frame_size的大小不应该超过tp_block_size,block的总数是tp_block_nr。
设置的缓存区几个注意点:
· 内存块大小tp_block_size必须按照页面大小对其,即必须是页面大小的整数倍;每个内存块至少要能够容纳一个数据包;
· 内存块数量tp_block_nr乘以每个内存块容纳的数据帧数目,应该等于数据包的总数tp_frame_nr。既 tp_block_size/tp_frame_size * tp_block_nr == tp_frame_nr
· 每个frame必须放在一个block中,每个block保存整数个frame。
例如:
tp_block_size= 4096
tp_frame_size= 2048
tp_block_nr = 4
tp_frame_nr = 8
得到的缓冲区结构应该如下:
3. mmap
char * buff = mmap(0, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
虽然那些buffer在内核中是由多个block组成的,但是映射后它们在用户空间中是连续的。
在每一个frame的开始有一个status域(可以查看struct tpacket_hdr),这些status定义在include/linux/if_packet.h中:
// Rx ring - header status 接收缓存区 每个frame的头部状态标识
#define TP_STATUS_KERNEL 0 // 标识内核可以处理
#define TP_STATUS_USER 1
#define TP_STATUS_COPY 2
#define TP_STATUS_LOSING 4
#define TP_STATUS_CSUMNOTREADY 8
struct tpacket_hdr {
unsigned long tp_status; // 头部状态标识
unsigned int tp_len; //接收包的长度(数据包的长度)
unsigned int tp_snaplen;
unsigned short tp_mac; // 如果设置的SOCK_RAW frame数据 + tp_mac 就会指向链路层头数据位置
unsigned short tp_net;// frame数据 + tp_net 就会指向除去链路层数据后,数据包的位置
unsigned int tp_sec;
unsigned int tp_usec;
};
状态这里我们只关心前两个,TP_STATUS_KERNEL和TP_STATUS_USER。如果status为TP_STATUS_KERNEL,表示这个frame可以被kernel使用,实际上就是可以将存放捕获的数据存放在这个frame中;如果status为TP_STATUS_USER,表示这个frame可以被用户空间使用,实际上就是这个frame中存放的是捕获的数据,应该读出来。
内核将所有的frame的status初始化为TP_STATUS_KERNEL,当内核接受到一个报文的时候,就选一个frame,把报文放进去,然后更新它的状态为TP_STATUS_USER(这里假设不出现其他问题,也就是忽略其他的状态)。用户程序读取报文,一旦报文被读取,用户必须将frame对应的status设置为0,也就是设置为TP_STATUS_KERNEL,这样内核就可以再次使用这个frame了。
而一个frame数据结构是什么样的呢?为了形象说明,借用了网上另一个大神的一张图
默认情况不设置时,头结构体对应struct tpacket_hdr ,其中tp_mac可以看出就是对应到链路层的偏移量,tp_net偏移到除去链路层的数据后的偏移量。而tp_len的长度跟SOCK_RAW,SOCK_DGRAM 模式有关。SOCK_RAW 对应从上图tp_mac开始到最后整个数据包,SOCK_DGRAM 则对应tp_net到最后数据包。
说了这么多,估计还是云里雾里,Don’t bb, show me code … 废话不多说还是看实例。
创建socket和设置bpf
int create_rev_socket(){
// dst 10.68.22.189 : 8888
struct sock_filter bpf_code[] = {
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 14, 0x00000800 },
{ 0x20, 0, 0, 0x0000001e },
{ 0x15, 0, 12, 0x0a4416bd },
{ 0x30, 0, 0, 0x00000017 },
{ 0x15, 2, 0, 0x00000084 },
{ 0x15, 1, 0, 0x00000006 },
{ 0x15, 0, 8, 0x00000011 },
{ 0x28, 0, 0, 0x00000014 },
{ 0x45, 6, 0, 0x00001fff },
{ 0xb1, 0, 0, 0x0000000e },
{ 0x48, 0, 0, 0x0000000e },
{ 0x15, 2, 0, 0x000022b8 },
{ 0x48, 0, 0, 0x00000010 },
{ 0x15, 0, 1, 0x000022b8 },
{ 0x6, 0, 0, 0x0000ffff },
{ 0x6, 0, 0, 0x00000000 }
};
int fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
struct sock_fprog bpf;
memset(&bpf,0x00,sizeof(bpf));
bpf.len = sizeof(bpf_code) / sizeof(struct sock_filter);
bpf.filter = bpf_code;
int ret = setsockopt( fd,SOL_SOCKET, SO_ATTACH_FILTER, &bpf,sizeof(bpf));
if (ret < 0)
{
printf("setsockopt( *sock_fd, SOL_SOCKET, SO_ATTACH_FILTER, &bpf, sizeof(bpf) err 3333\n");
}
return fd;
}
这里的bpf规则tcpdump篇章讨论过,不做详解。
2.,
void * rev_socket(void *param){
//network_chl * net_chl = (network_chl *)param;
int fd = create_rev_socket();
// 设置环形缓冲区
struct tpacket_req req;
req.tp_block_size = 4096;
req.tp_block_nr = BUFFER_SIZE/req.tp_block_size;
req.tp_frame_size = PER_PACKET_SIZE;
req.tp_frame_nr = BUFFER_SIZE/req.tp_frame_size;
int ret = setsockopt(fd, SOL_PACKET, PACKET_RX_RING, (void *)&req, sizeof(req));
if(ret<0)
{
printf("setsockopt failed\n");
return NULL;
}
// 建立内存映射,buff指针就已经指向了设置环形缓冲区的开始位置了
char * buff = (char *)mmap(0, BUFFER_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if(buff == MAP_FAILED)
{
perror("mmap");
close(fd);
return NULL;
}
int nIndex=0, i=0;
while(1)
{
//这里在poll前先检查是否已经有报文被捕获了
struct tpacket_hdr* pHead = (struct tpacket_hdr*)(buff+ nIndex*PER_PACKET_SIZE);
//如果frame的状态已经为TP_STATUS_USER了,说明已经在poll前已经有一个数据包被捕获了,如果poll后不再有数据包被捕获,那么这个报文不会被处理,这就是所谓的竞争情况。
if(pHead->tp_status == TP_STATUS_USER)
goto process_packet;
//poll检测报文捕获
struct pollfd pfd;
pfd.fd = fd;
//pfd.events = POLLIN|POLLRDNORM|POLLERR;
pfd.events = POLLIN;
pfd.revents = 0;
ret = poll(&pfd, 1, -1);
if(ret<0)
{
perror("poll");
munmap(buff, BUFFER_SIZE);
break;
}
process_packet:
//尽力的去处理环形缓冲区中的数据frame,直到没有数据frame了
for(i=0; i < req.tp_frame_nr; i++)
{
struct tpacket_hdr* pHead = (struct tpacket_hdr*)(buff+ nIndex*PER_PACKET_SIZE);
//XXX: 由于frame都在一个环形缓冲区中,因此如果下一个frame中没有数据了,后面的frame也就没有frame了
if(pHead->tp_status == TP_STATUS_KERNEL)
break;
if(pHead->tp_status == TP_STATUS_USER){
char temp[2048] ={0};
memcpy(temp,(char*)pHead+pHead->tp_mac,pHead->tp_len);
printf("tp_len:%d>>>>mac_len:%d>>>>net_len:%d>>>>snaplen:%d\n",pHead->tp_len,pHead->tp_mac,pHead->tp_net,pHead->tp_snaplen);
//处理数据frame
vg_packet_rev(temp,pHead->tp_len);
nrev ++;
nrev_byte =nrev_byte + pHead->tp_len -14;
//重新设置frame的状态为TP_STATUS_KERNEL
pHead->tp_len = 0;
pHead->tp_status = TP_STATUS_KERNEL;
}
//更新环形缓冲区的索引,指向下一个frame
nIndex++;
nIndex %= req.tp_frame_nr;
}
}
close(fd);
munmap(buff, BUFFER_SIZE);
return 0;
}
已经捕获数据包了 vg_packet_rev 处理其实就跟tcpdump中的数据处理流程相同。
int vg_packet_rev( char *packet,int lens){
printf("vg_packet_rev================enter\n");
struct ethernet *ethernet = (struct ethernet*) (packet);
const struct ndpi_llc_header_snap *llc;
u_short ethernet_type = ntohs(ethernet->ether_type);
int offset = sizeof(struct ethernet);
int pyld_eth_len = 0;
if(ethernet_type <= 1500){
pyld_eth_len = ethernet_type;
printf("================ethernet_type:%d\n",ethernet_type);
}
if(pyld_eth_len != 0) {
llc = (struct ndpi_llc_header_snap *)(&packet[offset]);
/* check for LLC layer with SNAP extension */
if(llc->dsap == SNAP || llc->ssap == SNAP) {
ethernet_type = llc->snap.proto_ID;
offset += + 8;
}
/* No SNAP extension - Spanning Tree pkt must be discarted */
else if(llc->dsap == BSTP || llc->ssap == BSTP) {
// printf("\n\nWARNING: only IPv4/IPv6 packets are supported in this demo (vg_security supports both IPv4 and IPv6), all other packets will be discarded\n\n");
return -1;
}
}
while (ethernet_type == ETH_P_8021Q) {
ethernet_type = (packet[offset + 2] << 8) + packet[offset + 3];
offset += 4;
}
int ether_off = offset;
struct ip * ip_header = (struct ip*) (packet + offset);
u_int ip_len = ntohs(ip_header->ip_len);
u_int ip_size = IP_HL(ip_header) * 4;
u_int msg_size = 0;
printf("msg_c2s_parse befor ===========enter sniffer-flow:src_ip%s\n",(char*)inet_ntoa(ip_header->ip_src));
printf("msg_c2s_parse befor ===========enter sniffer-flow:des_ip:%s\n",(char*)inet_ntoa(ip_header->ip_dst));
tsd_hdr_t psdHeader;
memset(&psdHeader,0,sizeof(tsd_hdr_t));
switch (ethernet_type) {
case ETH_P_IP:
switch (ip_header->ip_p) {
case IPPROTO_TCP: {
offset += ip_size;
struct tcp* tcp = (struct tcp*) (packet + offset);
u_int tcp_size = TH_OFF(tcp) * 4;
msg_size = ip_len - ip_size - tcp_size;
printf("befor===src_port:%d\n",ntohs(tcp->th_sport));
printf("befor===dst_port:%d\n",ntohs(tcp->th_dport));
}
break;
case IPPROTO_UDP:
offset += ip_size;
struct udphdr * udp = (struct udphdr*) (packet + offset);
printf("befor===src_port:%d\n",ntohs(udp->source));
printf("befor===dst_port:%d\n",ntohs(udp->dest));
offset = offset +SIZE_UDP_HEAD;
break;
default:
break;
}
break;
default:
break;
}
if(lens > PER_PACKET_SIZE){
printf("recv packet lens > PER_PACKET_SIZE failed\n");
return -1;
}
return 1;
}
这里运行结果就不展示了,为了实现发送packet_mmap中,楼主的接收示例已经修改过,在发送packet_mmap 中会将运行结果展示。(发送示例中,既有接收也有发送)
实现捕获抓包还是比较顺利的,网上对于packet_mmap捕获抓包还是有不少相关的博客。而本人实现的过程中有个问题,tcpdump中提到lo口抓包,其实是recvfrom后,将outgoing的包丢弃不处理的。从而收到的数据包不会出现相同,而packet_mmap压根没用recvfrom,那该如何实现呢?其实答案已经隐藏在前面提到的知识点。你仔细看一个frame的结构示意图,就可以看到每个frame还包括struct sockaddr_ll 结构体。只要你可以获取到返回包struct sockaddr_ll 结构体信息,处理方法就跟tcpdump 篇章中的一致就可以。
目前有 1 条留言 访客:0 条, 博主:0 条 ,引用: 1 条
外部的引用: 1 条
- linux下packet_mmap 中篇 (发送实现) | 求索阁