一 概述
前篇已经讲述了接收实现,而对于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 服务端上。下图是 发送客户端,和服务端的运行结果。
结果可以看出数据已经转发。
四 总结
注意点:
· 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会给你组装成一个完整的包。
至此发送讲解完。