1、Netfilter介绍
Netfilter是2.4.x内核引入的,工作在内核空间中,通常结合ip_table内核模块一起使用以构造linux下的防火墙。Linux内核中,netfilter是一个包过滤框架,默认地,它在这个框架上实现了包过滤、状态检测、网络地址转换和包标记等多种功能。因为它设计的开放性,任何有内核开发经验的开发人员,也可以很容易地利用它提供接口,在内核的数据链路层、网络层,实现自己的功能模块。
2、Hook点
Netfilter的整体思想为:嵌入到内核IP协议栈的一系列调用入口,通过挂钩,将钩子函数设置在报文处理的路径上,以达到控制数据包流通的作用。这些路径可以分为三类:流入的、流经的和流出的,具体为PRE_ROUTING,LOCAL_IN,FORWARD,LOCAL_OUT,POST_ROUTING,这些点,简单称之为Hook点。
Netfilter定义了一个二维数组structlist_headnf_hooks[NPROTO][NF_MAX_HOOKS]来保存各个协议的各个挂载点。每一个nf_hooks[协议号][hook点]元素都是一个链表头,所以链表中可以存在多个元素,每个元素都是一个包含有处理函数的结构体。
由于包总是从PRE_ROUTING/LOCAL_OUT两个钩子进入Netfilter,本文中把这两个钩子叫做入口。包总是从LOCAL_IN/POST_ROUTING两个钩子点走完Netfilter,把这两个钩子叫做出口。
3、Netfilter原理
Netfilter使用宏NF_HOOK在协议栈内部切入到Netfilter框架中,定义如下:
#defineNF_HOOK(pf,hook,skb,indev,outdev,okfn)\
NF_HOOK_THRESH(pf,hook,skb,indev,outdev,okfn,INT_MIN)
关于宏NF_HOOK各个参数的解释说明:
1)pf:协议族名,Netfilter架构同样可以用于IP层之外,因此这个变量还可以有诸如PF_INET6,PF_DECnet等名字。
2)hook:HOOK点的名字,对于IP层,就是取上面的五个值;
3)skb:不解释;
4)indev:数据包进来的设备,以structnet_device结构表示;
5)outdev:数据包出去的设备,以structnet_device结构表示;
(后面可以看到,以上五个参数将传递给nf_register_hook中注册的处理函数。)
6)okfn:是个函数指针,当所有的该HOOK点的所有登记函数调用完后,转而走此流程。
而NF_HOOK_THRESH又是一个宏:
#defineNF_HOOK_THRESH(pf,hook,skb,indev,outdev,okfn,thresh)\
({int__ret;\
if((__ret=nf_hook_thresh(pf,hook,&(skb),indev,outdev,okfn,thresh,1))==1)\
__ret=(okfn)(skb);\
__ret;})
NF_HOOK_THRESH宏内部调用了nf_hook_thresh函数。nf_hook_thresh通过传入的参数pf协议号和hook点,找到链表头指针nf_hooks[协议号][hook点],如图2-4所示,然后遍历调用在链表上注册过的函数hookfn,并根据hookfn的调用返回值的情况来进行相应处理。
由于多个函数都会被调用,并且被调用的函数还能决定该数据包的去留,所以存在一个优先级thresh(宏定义中),thresh来规定,哪个函数先被调用,哪个函数后被调用。优先级数值越小表示优先级越高,如果优先级一样,则按照注册的时候,挂载的先后顺序依次处理。见下面部分代码:
list_for_each_continue_rcu(*i,head){ /*遍历*/
structnf_hook_ops*elem=(structnf_hook_ops*)*i; /*获取元素*/
if(hook_thresh>elem->priority) /*优先级比hook_thresh高(值小)的执行*/
continue;
verdict=elem->hook(hook,skb,indev,outdev,okfn);/*调用hook函数*/
……
}
上述的元素是用到了下面这个结构体:
structnf_hook_ops{
structlist_headlist;
nf_hookfn*hook; /*回调函数*/
structmodule*owner; /*归属模块*/
u_int8_tpf; /*协议号*/
unsignedinthooknum; /*Hook点*/
intpriority; /*调用的优先级*/
};
注册一个钩子回调函数通过调用注册函数nf_register_hook来实现,nf_register_hook内部实现上插入到链表的时候,根据优先级priority进行排序插入,递归调用的时候,就直接使高优先级的先被调用,低优先级后被调用。
4、Netfilter切入点
1.net/ipv4/ip_input.c里的ip_rcv函数。该函数主要用来处理网络层的IP报文的入口函数,它到Netfilter框架的切入点为:
NF_HOOK(PF_INET,NF_IP_PRE_ROUTING,skb,dev,NULL,ip_rcv_finish)
如果协议栈当前收到了一个IP报文(PF_INET),那么就把这个报文传到Netfilter的NF_IP_PRE_ROUTING过滤点,去检查在过滤点(nf_hooks[2][0])是否已经有人注册了相关的用于处理数据包的钩子函数。如果有,则挨个去遍历链表nf_hooks[2][0]去寻找匹配的match和相应的target,根据返回到Netfilter框架中的值来进一步决定该如何处理该数据包(由钩子模块处理还是交由ip_rcv_finish函数继续处理)。
2.net/ipv4/ip_forward.c中的ip_forward函数,它的切入点为:
NF_HOOK(PF_INET,NF_IP_FORWARD,skb,skb->dev,rt->u.dst.dev,ip_forward_finish);
在经过路由抉择后,所有需要本机转发的报文都会交由ip_forward函数进行处理。这里,该函数由NF_IP_FOWARD过滤点切入到Netfilter框架,在nf_hooks[2][2]过滤点执行匹配查找。最后根据返回值来确定ip_forward_finish函数的执行情况。
3.net/ipv4/ip_output.c中的ip_output函数,它切入Netfilter框架的形式为:
NF_HOOK_COND(PF_INET,NF_IP_POST_ROUTING,skb,NULL,dev,ip_finish_output,!(IPCB(skb)->flags&IPSKB_REROUTED));
这里我们看到切入点从无条件宏NF_HOOK改成了有条件宏NF_HOOK_COND,调用该宏的条件是:如果协议栈当前所处理的数据包skb中没有重新路由的标记,数据包才会进入Netfilter框架。否则直接调用ip_finish_output函数走协议栈去处理。同样,如果需要陷入Netfilter框架则数据包会在nf_hooks[2][4]过滤点去进行匹配查找。
4.在net/ipv4/ip_input.c中的ip_local_deliver函数。该函数处理所有目的地址是本机的数据包,其切入函数为:
NF_HOOK(PF_INET,NF_IP_LOCAL_IN,skb,skb->dev,NULL,ip_local_deliver_finish);
发给本机的数据包,首先全部会去nf_hooks[2][1]过滤点上检测是否有相关数据包的回调处理函数,如果有则执行匹配和动作,最后根据返回值执行ip_local_deliver_finish函数。
5.net/ipv4/ip_output.c中的ip_push_pending_frames函数。该函数是将IP分片重组成完整的IP报文,然后发送出去。进入Netfilter框架的切入点为:
NF_HOOK(PF_INET,NF_IP_LOCAL_OUT,skb,NULL,skb->dst->dev,dst_output);
对于所有从本机发出去的报文都会首先去Netfilter的nf_hooks[2][3]过滤点去过滤。