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

linux下packet_mmap 中篇 (发送实现)

2019-09-25 18:52 工业·编程 ⁄ 共 11203字 ⁄ 字号 暂无评论

一 概述

前篇已经讲述了接收实现,而对于libpcap抓包的重要工具,本身其实也集成了packet_mmap抓包方式。那么既然可以用于捕获抓包。packet_mmap可以实现发送抓包吗?答案当然是肯定的。不过网上对于packet_mmap发送介绍少之又少。接下来会讲解packet_mmap发送数据包遇到的问题。

二 使用

packet_mmap原理跟接收原理一致,就不过重复讲述。唯一区别前篇原理中提到的状态标识,发送的状态标识跟接收的标识不一样。这后面会细说。

[setup]

· socket() ------> 捕获socket的创建

· setsockopt() ------> 设置发送环形缓冲区 PACKET_TX_RING

· bind() ------> 绑定发送的网口

· mmap() ------> 将分配的缓冲区映射到用户空间中

[capture]

· poll() ------> 等待底层有捕获到新的数据包

· send() ------> 将发送缓存中所有准备好发送的包发送。MSG_DONTWAIT 标志可通知内核底层立刻发送。

[shutdown]

· close ------> 关闭socket资源

这里你细心看下,会发现发送的packet_mmap比接收的多了两个步骤。一个是bind

一个是send().这两个步骤特别重要。楼主之前因为少了个bind步骤发现包一直发送不出去。后看了内核才发现底层会对包从哪个网口做判断。

1. 创建 socket

int fd = socket(PF_PACKET, mode, htons(ETH_P_ALL));

模式有两种,上一篇已经提到过,但是你要是自己发包,如果设置SOCK_RAW你就必须知道对方,自己的mac地址。这显然是很难做到的。因此这个模式发送建议都是使用SOCK_DGRAM 模式发包

2. 设置环形缓冲区

setsockopt(fd, SOL_PACKET, PACKET_TX_RING, (void *)&req, sizeof(req));

缓存区的的设置跟前篇接收缓存区的设置大同小异。不过多介绍。但是要注意的是,如果你是同一个创建的socket ,既要设置接收缓存区,和发送缓存区。

设置完后,发送缓存区和接收缓存区只能调用一次mmap映射内存。如下

...

setsockopt(fd, SOL_PACKET, PACKET_RX_RING, &foo, sizeof(foo));

setsockopt(fd, SOL_PACKET, PACKET_TX_RING, &bar, sizeof(bar));

...

rx_ring = mmap(0, size * 2, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

tx_ring = rx_ring + size;

同时映射出的内存,rx 缓存肯定是在前面的,tx缓存在后面。

3. mmap

char * buff = mmap(0, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

mmap调用其实都一样,但是调用后每个frame中,发送的头状态标识是不一样的

发送的头状态标识有四个,对应前篇提到过的struct tpacket_hdr结构体中tp_status变量

/* Tx ring - header status */

#define TP_STATUS_AVAILABLE 0x0   //  表示用户层可以往这个frame填充要发送的数据

#define TP_STATUS_SEND_REQUEST 0x1 // 表示内核底层可以发送这个frame数据

#define TP_STATUS_SENDING 0x2 // 表示底层通讯正在发送这个数据

#define TP_STATUS_WRONG_FORMAT 0x4 // 表示发送的数据格式有错误,不符合要求

4. bind

packet_mmap.txt 给了个bind的例子,如下

// Initialization example:

struct sockaddr_ll my_addr;

struct ifreq s_ifr;

strncpy (s_ifr.ifr_name, "eth0", sizeof(s_ifr.ifr_name));

/* get interface index of eth0 */

ioctl(this->socket, SIOCGIFINDEX, &s_ifr);

/* fill sockaddr_ll struct to prepare binding */

my_addr.sll_family = AF_PACKET;

my_addr.sll_protocol = htons(ETH_P_ALL);

my_addr.sll_ifindex =  s_ifr.ifr_ifindex;

/* bind socket to eth0 */

bind(this->socket, (struct sockaddr *)&my_addr, sizeof(struct sockaddr_ll));

三 应用

因为发送的包是需要符合格式的,而要是自己组装发送的包本质上也是可以的。不过既然你接收发送packet_mmap都理解了。结合上篇packet_mmap 接收实现。

示例实现了一个转发功能,既先接收到本机的数据包,然后封装数据包后再通过本机转发给另一台设备。

1.接收数据

接收流程跟上篇流程一致,只是接收到数据后处理会有所不同,不单单是打印一下。

static unsigned int g_port ;

int vg_packet_rev( char *packet,int lens,network_chl * net_chl){

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));

// 设置要转发的设备的ip地址,同时计算ip校验位

if(ip_header->ip_src.s_addr == net_chl->client_addr.n_ip){

printf("===========enter>>>>>>>>>>>>>net_chl->client_addr.n_ip\n");

ip_header->ip_dst.s_addr = net_chl->server_addr.n_ip;

}else{

ip_header->ip_dst.s_addr = net_chl->client_addr.n_ip;

}

ip_header->ip_src.s_addr = net_chl->porxy_addr_client.n_ip;

ip_header->ip_sum = 0;

printf("msg_c2s_parse after ===========enter sniffer-flow:src_ip%s\n",vg_sock_get_aip( &net_chl->porxy_addr_client));

printf("msg_c2s_parse after ===========enter sniffer-flow:des_ip:%s\n",(char*)inet_ntoa(ip_header->ip_dst));

ip_header->ip_sum = in_chksum((u_short *)ip_header,ip_size);

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;

// 设置目标的端口,和发送设备的源端口, 同时计算tcp 检验位

printf("befor===src_port:%d\n",ntohs(tcp->th_sport));

printf("befor===dst_port:%d\n",ntohs(tcp->th_dport));

if(tcp->th_dport ==  net_chl->porxy_addr_client.n_port ){

tcp->th_sport = net_chl->porxy_addr_server.n_port;

tcp->th_dport = g_port;

}else{

g_port = tcp->th_sport;

tcp->th_sport = net_chl->porxy_addr_client.n_port;

tcp->th_dport = net_chl->server_addr.n_port;

}

printf("after===src_port:%d\n",ntohs(tcp->th_sport));

printf("after===dst_port:%d\n",ntohs(tcp->th_dport));

tcp->th_sum = 0;

psdHeader.saddr=ip_header->ip_src.s_addr;

psdHeader.daddr=ip_header->ip_dst.s_addr;

psdHeader.mbz=0;

psdHeader.ptcl=ip_header->ip_p;

psdHeader.tcpl=htons(msg_size + tcp_size);

char szSendBuf[65535] = {0};

memcpy(szSendBuf, &psdHeader, sizeof(psdHeader));

memcpy(szSendBuf+sizeof(psdHeader), packet+offset,tcp_size);

memcpy(szSendBuf+sizeof(psdHeader)+tcp_size,packet+offset+tcp_size ,msg_size);

tcp->th_sum=in_chksum((u_short *)szSendBuf,sizeof(psdHeader)+tcp_size +msg_size);

// memcpy(packet+offset,tcp,sizeof(struct tcp));

}

break;

case IPPROTO_UDP:

offset += ip_size;

struct udphdr * udp = (struct udphdr*) (packet + offset);

// 设置目标的端口,和发送设备的源端口, 同时计算udp 检验位

printf("befor===src_port:%d\n",ntohs(udp->source));

printf("befor===dst_port:%d\n",ntohs(udp->dest));

if(udp->dest ==  net_chl->porxy_addr_client.n_port ){

udp->source =  net_chl->porxy_addr_server.n_port;

udp->dest =  net_chl->client_addr.n_port;

}else{

net_chl->client_addr.n_port = udp->source;

udp->source =  net_chl->porxy_addr_client.n_port;

udp->dest =  net_chl->server_addr.n_port;

}

udp->check = 0;

printf("after===src_port:%d\n",ntohs(udp->source));

printf("after===dst_port:%d\n",ntohs(udp->dest));

msg_size = ip_len - SIZE_IPNET - SIZE_UDP_HEAD;

psdHeader.saddr=ip_header->ip_src.s_addr;

psdHeader.daddr=ip_header->ip_dst.s_addr;

psdHeader.mbz=0;

psdHeader.ptcl=ip_header->ip_p;

psdHeader.tcpl=htons(8+msg_size);

char szSendBuf[65535] = {0};

memcpy(szSendBuf, &psdHeader, sizeof(psdHeader));

memcpy(szSendBuf+sizeof(psdHeader), udp, 8);

memcpy(szSendBuf+sizeof(psdHeader)+8, packet + offset +8, msg_size);

udp->check=in_chksum((u_short *)szSendBuf,sizeof(psdHeader)+8+msg_size);

// memcpy(packet+offset,udp,sizeof(struct udphdr));

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;

}else{

// 设置完后,将数据包添加到链表,到时候发送要用。

printf("lens:%d===================ether_off:%d>>>>>>>>\n",lens,ether_off);

vg_list_wlock(&net_chl->list_data);

net_data * rev_data = vg_malloc(sizeof(net_data));

rev_data->data = vg_malloc(lens-ether_off);

memcpy(rev_data->data,packet+ether_off,lens-ether_off);

rev_data->lens = lens-ether_off;

vg_list_add(&net_chl->list_data,rev_data);

vg_list_unlock(&net_chl->list_data);

}

return 1;

}

特别注意点:tcpdump中也提到ip校验和,但是设置了IP_HDRINCL 内核底层会自己计算ip层校验和。但是这里必须要用户自己计算。

2.发送数据包

既然已经拿到了需要发送的链表,那么关键点就是如何设置到发送缓存中,让内核发送用户想发送的数据?

其中还是先创建socket

int create_send_socket(){

int fd = socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL));

if (fd < 0)

{

printf("create send socket failed\n");

}

return fd;

}

而创建完之后有个关键步骤得bind发送的网口

int bind_send_socket(int fd){

struct sockaddr_ll my_addr;

struct ifreq s_ifr;

strncpy (s_ifr.ifr_name, "eth0", sizeof(s_ifr.ifr_name));

/* get interface index of eth0 */

int ret = ioctl(fd, SIOCGIFINDEX, &s_ifr);

if(ret < 0){

return -1;

}

/* fill sockaddr_ll struct to prepare binding */

my_addr.sll_family = AF_PACKET;

my_addr.sll_protocol = ETH_P_ALL;

my_addr.sll_ifindex =  s_ifr.ifr_ifindex;

/* bind socket to eth0 */

ret = bind(fd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr_ll));

if(ret <0 ){

return -1;

}

return  s_ifr.ifr_ifindex;

}

void * send_socket(void *param){

if(param == NULL){

return NULL;

}

network_chl *  net_chl = (network_chl *)param;

int fd = create_send_socket();

int i_ifindex = bind_send_socket(fd);

printf("bind_send_socket ret:%d\n ",i_ifindex);

// 设置发送缓存区

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_TX_RING, (void *)&req, sizeof(req) );

if(ret<0)

{

printf("setsockopt failed\n");

return NULL;

}

// 建立内存映射

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;

int j;

struct sockaddr_ll peer_addr;

peer_addr.sll_family = AF_PACKET;

peer_addr.sll_protocol = htons(ETH_P_IP);

peer_addr.sll_ifindex = i_ifindex;

uint8_t *data;

while(1)

{

// 判断发送链表是否有数据,只要有需要发送的数据,才进入发送主循环

if(net_chl->list_data.num_elt ==0){

printf("send_packet:%d>>send_bytes:%ld===============rev_packet:%d>>rev_bytes:%ld\n",

nsed,nsnd_byte,nrev,nrev_byte);

if(nIndex >= 1){

struct tpacket_hdr* pHead = (struct tpacket_hdr*)(buff+ (nIndex-1)*PER_PACKET_SIZE);

printf("nIndex:%d>>>>>pHead->tp_status:%ld\n",nIndex,pHead->tp_status);

}

sleep(2);

continue;

}

vg_list_iterator_t m_it;

net_data * tx_buffer = NULL;

// 如果状态TP_STATUS_SENDING 正在发送,调用poll等待数据发送完通知

struct tpacket_hdr* pHead = (struct tpacket_hdr*)(buff+ nIndex*PER_PACKET_SIZE);

if(pHead->tp_status == TP_STATUS_SENDING){

printf("enter TP_STATUS_SENDING\n");

struct pollfd pfd;

pfd.fd = fd;

pfd.revents = 0;

pfd.events = POLLOUT;

ret = poll(&pfd, 1, -1);

if(ret < 0){

perror("poll out ");

munmap(buff, BUFFER_SIZE);

break;

}

}else if(pHead->tp_status == TP_STATUS_AVAILABLE ){

printf("enter TP_STATUS_AVAILABLE\n");

}else{

printf("pHead->tp_status:%ld\n",pHead->tp_status);

}

vg_list_wlock(&net_chl->list_data);

tx_buffer = vg_list_get_first(&net_chl->list_data,&m_it);

j =0;

// 开始遍历发送缓存区,进入主循环

for(i=0 ; i < req.tp_frame_nr; i++)

{

if(tx_buffer && j<net_chl->list_data.num_elt){

struct tpacket_hdr* pHead = (struct tpacket_hdr*)(buff+ nIndex*PER_PACKET_SIZE);

data = (uint8_t*)pHead +TPACKET_ALIGN(sizeof(struct tpacket_hdr));

if(pHead->tp_status == TP_STATUS_AVAILABLE){

memcpy(data, tx_buffer->data, tx_buffer->lens );

pHead->tp_len = tx_buffer->lens;

printf("total_snd_byte:%ld======nIndex:%d||req.tp_frame_nr:%d====num_elt:%d====total_packet:%d=====>>>>lens:%d\n",

nsnd_byte, nIndex,req.tp_frame_nr,net_chl->list_data.num_elt,nsed,tx_buffer->lens);

pHead->tp_status = TP_STATUS_SEND_REQUEST;

j++;

nsed ++;

nsnd_byte += tx_buffer->lens;

if(tx_buffer->data){

vg_free(tx_buffer->data);

tx_buffer->data = NULL;

}

vg_free(tx_buffer);

vg_list_iterator_remove(&m_it);

tx_buffer = vg_list_get_next(&m_it);

}

nIndex++;

nIndex %= req.tp_frame_nr;

}else{

break;

}

}

vg_list_unlock(&net_chl->list_data);

ret =  sendto(fd, NULL, 0, MSG_DONTWAIT,  (struct sockaddr *) &peer_addr, sizeof(struct sockaddr_ll));

printf("send return lens:%d\n",ret);

sleep(1);

}

return NULL;

}

主循环就是讲链表中需要发的数据,填充到发送缓存区,填充后将链表中的数据移除。当链表中数据都填充进发送缓存区后,调用sendto通知内核底层,内核底层会将状态TP_STATUS_SEND_REQUEST的frame数据,发送出去。同时更新状态TP_STATUS_AVAILABLE 表示数据已发送,用户层可填充新的数据。

3. 运行结果

程序运行在 10.68.22.189 设备上。

当10.68.22.168 客户端连接 10.68.22.189:8888端口时,程序接收到这个数据包会给它转发到10.68.22.168:9999 服务端上。下图是 发送客户端,和服务端的运行结果。

wps18

结果可以看出数据已经转发。

四 总结

注意点:

· packet_mmap 发送的时候,要bind网口 。否则会发送数据会直接返回TP_STATUS_WRONG_FORMAT格式错误。

· packet_mmap 组包的时候,ip,tcp,udp校验和都要自己计算。

· 还有个误区,如果你发送的数据tcp流,数据发送超过mtu,就如上图结果显示发送的是10803数据大小。而packet_mmap接收到的数据最大就是mtu的数据大小(博主环境mtu 1500)也就是说10803 发送的数据,会通过分片,到达10.68.22.189本机设备。而packet_mmap接收到数据压根就不管组装包的。也就是packet_mmap发送还是接收都不组装,也不分片。不要以为你发送的tcp数据,到了packet_mmap会给你组装成一个完整的包。

至此发送讲解完。

给我留言

留言无头像?