现在的位置: 首页 > 自动控制 > 工业·编程 > 正文

linux下packet_mmap 前篇 (抓包实现)

2019-09-24 18:42 工业·编程 ⁄ 共 8061字 ⁄ 字号 评论 1 条

一 概述

· 前篇了解了libpcap库如何实现抓包

· 中篇深入内核底层讲述了抓包的原理

· 后篇自己实现过抓包过程

上述篇章中讲述的只是原始的抓包流程。

原始的抓包流程?简单的说就是创建socket,设置bpf后,每次接收数据包都要调用recvfrom系统调用。而每次调用recvfrom内核底层抓到的数据包都需要用内核copy到用户。不管是系统调用,还是copy都是相当耗cpu性能的。而linux内核提供了一种更高效的抓包方式packet_mmap.

二 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

得到的缓冲区结构应该如下:

wps16

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数据结构是什么样的呢?为了形象说明,借用了网上另一个大神的一张图

wps17

默认情况不设置时,头结构体对应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 中篇 (发送实现) | 求索阁

    给我留言

    留言无头像?