FROM: http://blog.csdn.net/beginning1126/article/details/15815339
原文链接:
http://www.2cto.com/Article/201206/136644.html
http://www.2cto.com/Article/201206/136645.html
http://os.51cto.com/art/201107/275664.htm
http://os.51cto.com/art/201107/273443_all.htm
本人研究linux的防火墙系统也有一段时间了,由于近来涉及到的工作比较纷杂,久而久之怕生熟了。趁有时间,好好把这方面的东西总结一番。一来是给自己做个沉淀,二来也欢迎这方面比较牛的前辈给小弟予以指点,共同学习,共同进步。
这里说明一下:本系列博文主要侧重于分析Netfilter的实现机制,原理和设计思想层面的东西,同时从用户态的iptables到内核态的Netfilter其交互过程和通信手段等。至于iptables的入门用法方面的东西,网上随便一搜罗就有一大堆,我这里不浪费笔墨了。很多人在接触iptables之后就会这么一种感觉:我通过iptables命令配下去的每一条规则,到底是如何生效的呢?内核又是怎么去执行这些规则匹配呢?如果iptables不能满足我当下的需求,那么我是否可以去对其进行扩展呢?这些问题,都是我在接下来的博文中一一和大家分享的话题。这里需要指出:因为Netfilter与IP协议栈是无缝契合的,所以如果你要是有协议栈方面的基础,在阅读本文时一定会感觉轻车熟路。当然,如果没有也没关系,因为我会在关键点就协议栈的入门知识给大家做个普及。只是普及哦,不会详细深入下去的,因为涉及的东西太多了,目前我还正在研究摸索当中呢。好了,废话不多说,进入正题。
备注:我研究的内核版本是2.6.21,iptables的版本1.4.0。
什么是Netfilter?
为了说明这个问题,首先看一个网络通信的基本模型:
![\](http://up.2cto.com/2012/0621/20120621010219638.jpg)
在数据的发送过程中,从上至下依次是“加头”的过程,每到达一层数据就被会加上该层的头部;与此同时,接受数据方就是个“剥头”的过程,从网卡收上包来之后,在往协议栈的上层传递过程中依次剥去每层的头部,最终到达用户那儿的就是裸数据了。
那么,“栈”模式底层机制基本就是像下面这个样子:
![\](http://up.2cto.com/2012/0621/20120621010225922.jpg)
对于收到的每个数据包,都从“A”点进来,经过路由判决,如果是发送给本机的就经过“B”点,然后往协议栈的上层继续传递;否则,如果该数据包的目的地是不本机,那么就经过“C”点,然后顺着“E”点将该包转发出去。
对于发送的每个数据包,首先也有一个路由判决,以确定该包是从哪个接口出去,然后经过“D”点,最后也是顺着“E”点将该包发送出去。
协议栈那五个关键点A,B,C,D和E就是我们Netfilter大展拳脚的地方了。
Netfilter是Linux 2.4.x引入的一个子系统,它作为一个通用的、抽象的框架,提供一整套的hook函数的管理机制,使得诸如数据包过滤、网络地址转换(NAT)和基于协议类型的连接跟踪成为了可能。Netfilter在内核中位置如下图所示:
![\](http://up.2cto.com/2012/0621/20120621010227925.jpg)
这幅图,很直观的反应了用户空间的iptables和内核空间的基于Netfilter的ip_tables模块之间的关系和其通讯方式,以及Netfilter在这其中所扮演的角色。
回到前面讨论的关于协议栈那五个关键点“ABCDE”上来。Netfilter在netfilter_ipv4.h中将这个五个点重新命了个名,如下图所示,意思我就不再解释了,猫叫咪咪而已:
![\](http://up.2cto.com/2012/0621/20120621010229737.jpg)
在每个关键点上,有很多已经按照优先级预先注册了的回调函数(后面再说这些函数是什么,干什么用的。有些人喜欢把这些函数称为“钩子函数”,说的是同一个东西)埋伏在这些关键点,形成了一条链。对于每个到来的数据包会依次被那些回调函数“调戏”一番再视情况是将其放行,丢弃还是怎么滴。但是无论如何,这些回调函数最后必须向Netfilter报告一下该数据包的死活情况,因为毕竟每个数据包都是Netfilter从人家协议栈那儿借调过来给兄弟们Happy的,别个再怎么滴也总得“活要见人,死要见尸”吧。每个钩子函数最后必须向Netfilter框架返回下列几个值其中之一:
n NF_ACCEPT 继续正常传输数据报。这个返回值告诉Netfilter:到目前为止,该数据包还是被接受的并且该数据包应当被递交到网络协议栈的下一个阶段。
n NF_DROP 丢弃该数据报,不再传输。
n NF_STOLEN 模块接管该数据报,告诉Netfilter“忘掉”该数据报。该回调函数将从此开始对数据包的处理,并且Netfilter应当放弃对该数据包做任何的处理。但是,这并不意味着该数据包的资源已经被释放。这个数据包以及它独自的sk_buff数据结构仍然有效,只是回调函数从Netfilter 获取了该数据包的所有权。
n NF_QUEUE 对该数据报进行排队(通常用于将数据报给用户空间的进程进行处理)
n NF_REPEAT 再次调用该回调函数,应当谨慎使用这个值,以免造成死循环。
为了让我们显得更专业些,我们开始做些约定:上面提到的五个关键点后面我们就叫它们为hook点,每个hook点所注册的那些回调函数都将其称为hook函数。
Linux 2.6版内核的Netfilter目前支持IPv4、IPv6以及DECnet等协议栈,这里我们主要研究IPv4协议。关于协议类型,hook点,hook函数,优先级,通过下面这个图给大家做个详细展示:
![\](http://up.2cto.com/2012/0621/20120621010235525.jpg)
对于每种类型的协议,数据包都会依次按照hook点的方向进行传输,每个hook点上Netfilter又按照优先级挂了很多hook函数。这些hook函数就是用来处理数据包用的。
Netfilter使用NF_HOOK(include/linux/netfilter.h)宏在协议栈内部切入到Netfilter框架中。相比于2.4版本,2.6版内核在该宏的定义上显得更加灵活一些,定义如下:
#define NF_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:数据包进来的设备,以struct net_device结构表示;
5) outdev:数据包出去的设备,以struct net_device结构表示;
(后面可以看到,以上五个参数将传递给nf_register_hook中注册的处理函数。)
6) okfn:是个函数指针,当所有的该HOOK点的所有登记函数调用完后,转而走此流程。
而NF_HOOK_THRESH又是一个宏:
#define NF_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宏只增加了一个thresh参数,这个参数就是用来指定通过该宏去遍历钩子函数时的优先级,同时,该宏内部又调用了nf_hook_thresh函数:
static inline int nf_hook_thresh(int pf, unsigned int hook,
struct sk_buff **pskb,
struct net_device *indev,
struct net_device *outdev,
int (*okfn)(struct sk_buff *), int thresh,
int cond)
{
if (!cond)
return 1;
#ifndef CONFIG_NETFILTER_DEBUG
if (list_empty(&nf_hooks[pf][hook]))
return 1;
#endif
return nf_hook_slow(pf, hook, pskb, indev, outdev, okfn, thresh);
}
这个函数又只增加了一个参数cond,该参数为0则放弃遍历,并且也不执行okfn函数;为1则执行nf_hook_slow去完成钩子函数okfn的顺序遍历(优先级从小到大依次执行)。
在net/netfilter/core.h文件中定义了一个二维的结构体数组,用来存储不同协议栈钩子点的回调处理函数。
struct list_head nf_hooks[NPROTO][NF_MAX_HOOKS];
其中,行数NPROTO为32,即目前内核所支持的最大协议簇;列数NF_MAX_HOOKS为挂载点的个数,目前在2.6内核中该值为8。nf_hooks数组的最终结构如下图所示。
在include/linux/socket.h中IP协议AF_INET(PF_INET)的序号为2,因此我们就可以得到TCP/IP协议族的钩子函数挂载点为:
PRE_ROUTING: nf_hooks[2][0]
LOCAL_IN: nf_hooks[2][1]
FORWARD: nf_hooks[2][2]
LOCAL_OUT: nf_hooks[2][3]
POST_ROUTING: nf_hooks[2][4]
同时我们看到,在2.6内核的IP协议栈里,从协议栈正常的流程切入到Netfilter框架中,然后顺序、依次去调用每个HOOK点所有的钩子函数的相关操作有如下几处:
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过滤点,去检查[R]在那个过滤点(nf_hooks[2][0])是否已经有人注册了相关的用于处理数据包的钩子函数。如果有,则挨个去遍历链表nf_hooks[2][0]去寻找匹配的match和相应的target,根据返回到Netfilter框架中的值来进一步决定该如何处理该数据包(由钩子模块处理还是交由ip_rcv_finish函数继续处理)。
[R]:刚才说到所谓的“检查”。其核心就是nf_hook_slow()函数。该函数本质上做的事情很简单,根据优先级查找双向链表nf_hooks[][],找到对应的回调函数来处理数据包:
struct list_head **i;
list_for_each_continue_rcu(*i, head) {
struct nf_hook_ops *elem = (struct nf_hook_ops *)*i;
if (hook_thresh > elem->priority)
continue;
verdict = elem->hook(hook, skb, indev, outdev, okfn);
if (verdict != NF_ACCEPT) { … … }
return NF_ACCEPT;
}
上面的代码是net/netfilter/core.c中的nf_iterate()函数的部分核心代码,该函数被nf_hook_slow函数所调用,然后根据其返回值做进一步处理。
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]过滤点去过滤。一般情况下来来说,不管是 路由器 还是PC中端,很少有人限制自己机器发出去的报文。因为这样做的潜在风险也是显而易见的,往往会因为一些不恰当的设置导致某些服务失效,所以在这个过滤点上拦截数据包的情况非常少。当然也不排除真的有特殊需求的情况。
小节:整个 Linux 内核中Netfilter框架的HOOK机制可以概括如下:
在数据包流经内核协议栈的整个过程中,在一些已预定义的关键点上PRE_ROUTING、LOCAL_IN、FORWARD、LOCAL_OUT和POST_ROUTING会根据数据包的协议簇PF_INET到这些关键点去查找是否注册有钩子函数。如果没有,则直接返回okfn函数指针所指向的函数继续走协议栈;如果有,则调用nf_hook_slow函数,从而进入到Netfilter框架中去进一步调用已注册在该过滤点下的钩子函数,再根据其返回值来确定是否继续执行由函数指针okfn所指向的函数。
![\](http://up.2cto.com/2012/0621/20120621010404685.jpg)
从上图我们可以看出,ip_tables模块它是防火墙的核心模块,负责维护防火墙的规则表,通过这些规则,实现防火墙的核心功能。归纳起来,主要有三种功能:包过滤(filter)、NAT以及包处理(mangle)。同进该模块留有与用户空间通讯的接口。如第一篇博文中Netfilter处于内核中位置那副图所描述的情形。
在内核中我们习惯将上述的filter,nat和mangle等称之为模块。连接跟踪conntrack有些特殊,它是NAT模块和状态防火墙的功能基础,其实现机制我们也会在后面详细分析的。
OK,回到开篇的问题,我们来看一下基于Netfilter的防火墙系统到底定义了哪些钩子函数?而这些钩子函数都是分别挂载在哪些hook点的?按照其功能结构划分,我将这些hook函数总结如下:
![\](http://up.2cto.com/2012/0621/20120621010406598.jpg)
包过滤子功能:包过滤一共定义了四个hook函数,这四个hook函数本质最后都调用了ipt_do_table()函数。
网络地址转换子功能:该模块也定义了四个hook函数,其中有三个最终也都调用了ip_nat_fn()函数,ip_nat_adjust()有自己另外的功能。
连接跟踪子功能:这里连接跟踪应该称其为一个子系统更合适些。它也定义四个hook函数,其中ip_conntrack_local()最后其实也调用了ip_conntrack_in()函数。
以上便是Linux的防火墙---iptables在内核中定义的所有hook函数。接下来我们再梳理一下这些hook函数分别是被挂载在哪些hook点上的。还是先贴个三维框图,因为我觉得这个图是理解Netfilter内核机制最有效,最直观的方式了,所以屡用不爽!
![\](http://up.2cto.com/2012/0621/20120621010408542.jpg)
然后,我们拿一把大刀,从协议栈的IPv4点上顺着hook点延伸的方向一刀切下去,就会得到一个平面,如上图所示。前面这些hook函数在这个平面上的分布情况如下所示:
![\](http://up.2cto.com/2012/0621/20120621010411419.jpg)
这幅图彻底暴露了ip_tables内核模块中那些hook函数在各个hook点分布情况。与此同时,这个图还告诉了我们很多信息:所有由网卡收上来的数据包率先被ip_conntrack_defrag处理;链接跟踪系统的入口函数以-200的优先级被注册到了PRE_ROUTING和LOCAL_OUT两个hook点上,且其优先级高于mangle操作,NAT和包过滤等其他模块;DNAT可以在PRE_ROUTING和LOCAL_OUT两个hook点来做,SNAT可以在LOCAL_IN和POST_ROUTING两个hook点上。如果你认真研究会发现这个图确实很有用。因为当初为了画这个图我可是两个晚上没睡好觉啊,画出来后还要验证自己的想法,就得一步一步给那些关键的hook点和hook函数分别加上调试打印信息,重新编译内核然后确认这些hook函数确实是按照我所分析的那样被调用的。因为对学术严谨就是对自己负责,一直以来我也都这么坚信的。“没有调查就没发言权”;在我们IT行业,“没有亲自动手做过就更没有发言权”。又扯远了,赶紧收回来。
框架的东西多看些之上从宏观上可以使我们对整个系统的架构和设计有个比较全面的把握,接下来在分析每个细节的时候才会做到心中有数,不至于“盲人摸象”的境地。在本章即将结束之际,我们来看点代码级的东西。我保证只是个简单的入门了解,因为重头戏我打算放到后面,大家也知道分析代码其实是最头疼的,关键还是看自己的心态。
Netfilter的实现方式:
第一篇我们讲了Netfilter的原理,这里我们谈谈其实现机制的问题。
我们回头分析一下那个用于存储不同协议簇在每个hook点上所注册的hook函数链的二维数组nf_hooks[][],其类型为list_head: www.2cto.com
struct list_head nf_hooks[NPROTO][NF_MAX_HOOKS];
list_head{}结构体定义在include/linux/list.h头文件中
struct list_head {
struct list_head *next, *prev;
};
这是Linux内核中处理双向链表的标准方式。当某种类型的数据结构需要被组织成双向链表时,会在该数据结构的第一个字段放置一个list_head{}类型的成员。在后面的使用过程中可以通过强制类型转换来实现双向链表的遍历操作。
在Netfilter中一个非常重要的数据结构是nf_hook_ops{} <include/linux/netfilter.h>:
struct nf_hook_ops
{
struct list_head list;
/* User fills in from here down. */
nf_hookfn *hook;
struct module *owner;
int pf;
int hooknum;
/* Hooks are ordered in ascending priority. */
int priority;
};
对该结构体中的成员参数做一下解释:
n list:因为在一个HOOK点有可能注册多个钩子函数,因此这个变量用来将某个HOOK点所注册的所有钩子函数组织成一个双向链表;
n hook:该参数是一个指向nf_hookfn类型的函数的指针,由该函数指针所指向的回调函数在该hook被激活时调用【nf_hookfn在后面做解释】;
n owner:表示这个hook是属于哪个模块的
n pf:该hook函数所处理的协议。目前我们主要处理IPv4,所以该参数总是PF_INET;
n hooknum:钩子函数的挂载点,即HOOK点;
n priority:优先级。前面也说过,一个HOOK点可能挂载了多个钩子函数,当Netfilter在这些HOOK点上遍历查找所注册的钩子函数时,这些钩子函数的先后执行顺序便由该参数来制定。
nf_hookfn所定义的回调函数的原型在include/linux/netfilter.h文件中:
typedef unsigned int nf_hookfn(unsigned int hooknum, //HOOK点
struct sk_buff **skb, //不解释
const struct net_device *in, //数据包的网络如接口
const struct net_device *out, //数据包的网络出接口
int (*okfn)(struct sk_buff *)); //后续的处理函数
我们可以到,上面这五个参数最后将由NF_HOOK或NF_HOOK_COND宏传递到Netfilter框架中去。
如果要增加新的钩子函数到Netfilter中相应的过滤点,我们要做的工作其实很简单:
1)、编写自己的钩子函数;
2)、实例化一个struct nf_hook_ops{}结构,并对其进行适当的填充,第一个参数list并不是用户所关心的,初始化时必须设置成{NULL,NULL};
3)、用nf_register_hook()<net/netfilter/core.c>函数将我们刚刚填充的nf_hook_ops结构体注册到相应的HOOK点上,即nf_hooks[prot][hooknum]。
这也是最原生的扩展方式。有了上面这个对nf_hook_ops{}及其用法的分析,后面我们再分析其他模块,如filter模块、nat模块时就会不那么难懂了。
内核在网络协议栈的关键点引入NF_HOOK宏,从而搭建起了整个Netfilter框架。但是NF_HOOK宏仅仅只是一个跳转而已,更重要的内容是“内核是如何注册钩子函数的呢?这些钩子函数又是如何被调用的呢?谁来维护和管理这些钩子函数?”