内核数据包处理.docx
《内核数据包处理.docx》由会员分享,可在线阅读,更多相关《内核数据包处理.docx(15页珍藏版)》请在冰豆网上搜索。
内核数据包处理
数据包处理的一些建议
前言
我们大部分功能都需要解析数据,进行一系列的包匹配完成,但是目前,我们没有一个很好的框架来简化这个过程,大家处理数据包都是采用原生的linux内核接口,并且没有统一的规范要求如何使用这些接口,所以,存在大量的陷阱,一不留神就造成宕机。
获取IP头部
iph=ip_hdr(skb);
structsk_buff{
......
sk_buff_data_ttransport_header;/*Transportlayerheader*/
sk_buff_data_tnetwork_header;/*Networklayerheader*/
sk_buff_data_tmac_header;/*Linklayerheader*/
......
}
1)__netif_receive_skb()在进入三层处理前就对network_header进行了设置。
2)ip_rcv()中详细的检查保证了IP头部到netfilter后是完整的。
3)netfilter可以尽情使用ip头部。
获取tcp头部
错误1:
tcph=tcp_hdr(skb);
陷阱:
netfilter的钩子点是属于TCP/IP协议栈的三层流程中,而四层的TCP头部此时还没有正确获取,只是初始化为IP头部的值,无法直接使用。
错误2:
tcph=(char*)iph+(iph->ihl<<2);
陷阱:
数据包可能是非线性的
改进:
tcpoff=skb_network_offset(skb)+(iph->ihl<<2);
tcph=skb_header_pointer(skb,tcpoff,sizeof(_tcph),&_tcph);
if(tcph==NULL)
return;
接口介绍:
skb_network_offset(structskb_buff*skb)
计算三层头部相对于skb->data的偏移
void*skb_header_pointer(structsk_buff*skb,intoffset,intlen,void*buffer)
从skb的指定偏移取制定长度的数据,如果要取的数据位于线性区,直接返回其开始指针,否则,则拷贝到buffer中,并将buffer指针返回。
printk("%pI4%d----->%pI4%dlen:
%dID:
%d\n",
&iph->saddr,
ntohs(tcph->source),
&iph->daddr,
ntohs(tcph->dest),
ntohs(iph->tot_len),
ntohs(iph->id));
打印信息
注意要点:
1)IP地址输出
Ipv4:
%pI4%pi4
IPv6:
%pI6%pi6
2)MAC地址
%pM%pm
3)字节序的转换
ntohs()ntohl()htons()htonl()
__const_ntohl()__const_ntohs()__const_htonl()__const_htons()
区别:
__const_*()是编译时处理的。
获取TCP负载
风险:
payload=(char*)tcph+tcph->doff*4;
陷阱1:
数据包可能是非线性的,同TCP头部。
陷阱2:
TCP头部数据有可能是被篡改过的,tcph->doff如果很大怎么办?
改进1:
tcplen=skb->len-tcpoff;
if(tcph->doff*4doff*4)
{
printk("Badtcp.\n");
returnNF_ACCEPT;
}
if(skb_is_nonlinear(skb))
{
printk("Nonlinearskb.\n");
returnNF_ACCEPT;
}
payload=(char*)tcph+tcph->doff*4;
payload_len=tcplen-tcph->doff*4;
if(payload_len==0)
returnNF_ACCEPT;
接口介绍:
intskb_is_nonlinear(structsk_buff*skb)
判断skb的数据是否是非线性的
改进2:
charpayload_buf[1500];
tcplen=skb->len-tcpoff;
if(tcph->doff*4doff*4)
{
printk("Badtcp.\n");
returnNF_ACCEPT;
}
payload_len=tcplen-tcph->doff*4;
if(payload_len==0)
returnNF_ACCEPT;
if(payload_len>sizeof(payload_buf))
returnNF_ACCEPT;
payload=skb_header_pointer(skb,tcpoff+tcph->doff*4,payload_len,payload_buf);
if(payload==NULL)
returnNF_ACCEPT;
改进3:
tcplen=skb->len-tcpoff;
if(tcph->doff*4doff*4)
{
printk("Badtcp.\n");
returnNF_ACCEPT;
}
if(skb_linearize(skb))
{
printk("Cannotlinearizeskb.\n");
returnNF_ACCEPT;
}
payload=(char*)tcph+tcph->doff*4;
payload_len=tcplen-tcph->doff*4;
if(payload_len==0)
returnNF_ACCEPT;
接口介绍:
intskb_linearize(structsk_buff*skb)
将skb线性化
解析数据
1)判断数据包内容
风险1:
if(payload[0]!
='G'||payload[1]!
='E'||payload[2]!
='T')
风险2:
if((payload[0]=='G'&&payload[1]=='E'&&payload[2]=='T')&&payload_len==3)
陷阱:
如果payload的长度只有1个字节怎么办?
改进:
if(payload_len<3||payload[0]!
='G'||payload[1]!
='E'||payload[2]!
='T')
2)查找数据包中的某个字符串
风险:
host=strstr(payload,"Host:
");
陷阱:
可能会越界,数据包不一定是以'\0'结束。
改进:
host=strnstr(payload,"Host:
",payload_len);
if(host==NULL)
return;
一定要使用这一系列的函数:
strnchr
strncpy
strncat
strncmp
strnicmp
strnlen
memcpy
3)移动指向数据包的指针
风险:
host=strnstr(payload,"Host:
",payload_len);
if(host==NULL)
return;
host=host+sizeof("Host:
")-1;
/*
*dealwithhost
*/
陷阱:
查找的字符串有可能是数据包的最后一部分。
改进:
host=strnstr(payload,"Host:
",payload_len);
if(host==NULL)
return;
host=host+sizeof("Host:
")-1;
if(host>=(payload+payload_len))
return;
/*
*dealwithhost
*/
4)数据包操作
错误:
u32len;
len=payload_len-512;
if(len<=0)
return;
memcpy(buf,payload,len);
陷阱:
无符号数的强制类型转换,u32类型永远都是大于等于0的,当payload_len小于512时,判断就会不生效。
改进:
u32len;
if(payload_len<=512)
return;
len=payload_len-512;
memcpy(buf,payload,len);
或者
intlen;
len=payload_len-512;
if(len<=0)
return;
memcpy(buf,payload,len);
5)
风险:
intlen=payload[1];
memcpy(buf,&payload[2],len);
陷阱:
可能是异常数据包,offset不是你想要的
正确做法:
intlen=payload[1];
if(len>=payload_len-2)
return;
memcpy(buf,&payload[2],len);
综述:
数据包处理要时刻保持警醒,它可能不是你想象的样子!
内存分配
风险:
void*xxx_alloc(unsignedlongsize,gfp_tflags)
{
intmalloc_size=size+sizeof(unsignedlong);
void*p;
if(memory_use+malloc_size>memory_max)
return;
p=kmalloc(malloc_size,flags);
if(p==NULL)
returnNULL;
memory_use+=malloc_size;
*p=size;
p+=sizeof(unsignedlong);
returnp;
}
陷阱1:
如果需要申请的size是0会如何?
陷阱2:
多个cpu并发申请又会如何?
改进:
void*xxx_alloc(unsignedlongsize,gfp_tflags)
{
intmalloc_size=size+sizeof(unsignedlong);
void*p;
intuse;
if(size==0)
returnNULL;
use=atomic_read(&memory_use);
if(use+malloc_size>memory_max)
return;
p=kmalloc(malloc_size,flags);
if(p==NULL)
returnNULL;
atomic_add(malloc_size,&memory_use);
*p=size;
p+=sizeof(unsignedlong);
returnp;
}
问题:
kmalloc(0,...)返回值是什么?
建议:
相同的内存反复申请释放的情况下,请使用kmem_cache_alloc
建议的同步与互斥方法
1)rcu锁
使用场景:
进程上下文用来配置,软中断上下文只读配置的情况
好处:
性能高,接口简单
方法:
hook函数读取配置,中断上下文:
rcu_read_lock();
data=rcu_dereference(global_data);
/*
*dealwithdata
*/
rcu_read_unlock();
基于proc文件等的配置下发,进程上下文:
data=kmalloc(size,GFP_KERNEL);
if(data==NULL)
return;
if(copy_from_user(data,user,len))
return
old_data=rcu_dereference(global_data)
rcu_assign_pointer(global_data,data);
if(old_data)
{
synchronize_rcu();
kfree(old_data);
}
另一种方法:
if(old_data)
call_rcu(&old_data->rcu,data_free_rcu);
staticdata_free_rcu(structrcu_head*head)
{
structxxx_data*data=container_of(head,structxxx_data,rcu);
kfree(data);
}
注意1:
synchronize_rcu()只能用于进程上下文,call_rcu()可以用于中断上下文。
注意2:
data_free_rcu的调用是软中断上下文,不能使用vfree。
模块卸载:
old_data=rcu_dereference(global_data)
rcu_assign_pointer(global_data,NULL);
if(old_data)
{
synchronize_rcu();
kfree(old_data);
}
2)每CPU变量
使用场景:
在钩子函数中使用的临时缓存区,不用每次申请释放,使用全局变量。
方法:
hook函数:
cpu=get_cpu()
data=per_cpu_ptr(global_data,cpu);
/*
*dealwithdata
*/
put_cpu();
模块加载:
global_data=alloc_percpu(char[1024])
模块卸载:
free_percpu(global_data)
注意:
alloc_percpu()上限32k