(一)洞悉linux下的Netfilter&iptables:什么是Netfilter?

转自:http://blog.chinaunix.net/uid-23069658-id-3160506.htmlhtml

 

本人研究linux的防火墙系统也有一段时间了,因为近来涉及到的工做比较纷杂,长此以往怕生熟了。趁有时间,好好把这方面的东西总结一番。一来是给本身作个沉淀,二来也欢迎这方面比较牛的前辈给小弟予以指点,共同窗习,共同进步。linux

    能在CU上混的人绝非等闲之辈。所以,小弟这里说明一下:本系列博文主要侧重于分析Netfilter的实现机制,原理和设计思想层面的东西,同时从用户态的iptables到内核态的Netfilter其交互过程和通讯手段等。至于iptables的入门用法方面的东西,网上随便一搜罗就有一大堆,我这里不浪费笔墨了。数组

    不少人在接触iptables以后就会这么一种感受:我经过iptables命令配下去的每一条规则,究竟是如何生效的呢?内核又是怎么去执行这些规则匹配呢?若是iptables不能知足我当下的需求,那么我是否能够去对其进行扩展呢?这些问题,都是我在接下来的博文中一一和你们分享的话题。这里须要指出:由于Netfilter与IP协议栈是无缝契合的,因此若是你要是有协议栈方面的基础,在阅读本文时必定会感受轻车熟路。固然,若是没有也不要紧,由于我会在关键点就协议栈的入门知识给你们作个普及。只是普及哦,不会详细深刻下去的,由于涉及的东西太多了,目前我还正在研究摸索当中呢。好了,废话很少说,进入正题。网络

备注:我研究的内核版本是2.6.21,iptables的版本1.4.0。数据结构

 

什么是Netfilter?架构

    为了说明这个问题,首先看一个网络通讯的基本模型:app

 

    在数据的发送过程当中,从上至下依次是“加头”的过程,每到达一层数据就被会加上该层的头部;与此同时,接受数据方就是个“剥头”的过程,从网卡收上包来以后,在往协议栈的上层传递过程当中依次剥去每层的头部,最终到达用户那儿的就是裸数据了。框架

那么,“栈”模式底层机制基本就是像下面这个样子:socket

 

对于收到的每一个数据包,都从“A”点进来,通过路由判决,若是是发送给本机的就通过“B”点,而后往协议栈的上层继续传递;不然,若是该数据包的目的地是不本机,那么就通过“C”点,而后顺着“E”点将该包转发出去。函数

对于发送的每一个数据包,首先也有一个路由判决,以肯定该包是从哪一个接口出去,而后通过“D”点,最后也是顺着“E”点将该包发送出去。

协议栈那五个关键点A,B,C,D和E就是咱们Netfilter大展拳脚的地方了。

Netfilter是Linux 2.4.x引入的一个子系统,它做为一个通用的、抽象的框架,提供一整套的hook函数的管理机制,使得诸如数据包过滤、网络地址转换(NAT)和基于协议类型的链接跟踪成为了可能。Netfilter在内核中位置以下图所示:

 

这幅图,很直观的反应了用户空间的iptables和内核空间的基于Netfilter的ip_tables模块之间的关系和其通信方式,以及Netfilter在这其中所扮演的角色。

回到前面讨论的关于协议栈那五个关键点“ABCDE”上来。Netfilter在netfilter_ipv4.h中将这个五个点从新命了个名,以下图所示,意思我就再也不解释了,猫叫咪咪而已:

 

在每一个关键点上,有不少已经按照优先级预先注册了的回调函数(后面再说这些函数是什么,干什么用的。有些人喜欢把这些函数称为“钩子函数”,说的是同一个东西)埋伏在这些关键点,造成了一条链。对于每一个到来的数据包会依次被那些回调函数“调戏”一番再视状况是将其放行,丢弃仍是怎么滴。可是不管如何,这些回调函数最后必须向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目前支持IPv四、IPv6以及DECnet等协议栈,这里咱们主要研究IPv4协议。关于协议类型,hook点,hook函数,优先级,经过下面这个图给你们作个详细展现:

 

 

对于每种类型的协议,数据包都会依次按照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所指向的函数。

未完,待续…

相关文章
相关标签/搜索