洞悉linux下的Netfilter&iptables

分类: LINUX

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

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

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

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

 

什么是Netfilter?

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

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

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

对于收到的每个数据包,都从“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目前支持IPv4、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_INETNF_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_INETNF_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_INETNF_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_INETNF_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_INETNF_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所指向的函数。

未完,待续…

 Netfilter框架为内核模块参与IP层数据包处理提供了很大的方便,内核的防火墙模块(ip_tables)正是通过把自己所编写的一些钩子函数注册到Netfilter所监控的五个关键点(NF_IP_PRE_ROUTING,

NF_IP_LOCAL_IN,NF_IP_FORWARD, NF_IP_LOCAL_OUT,NF_IP_POST_ROUTING) 这种方式介入到对数据包的处理。这些钩子函数功能非常强大,按功能可分为四大类:连接跟踪、数据包的过滤、网络地址转换 (NAT) 和数据包的修改。它们之间的关系,以及和 Netfilter ip_tables 难舍难分的缠绵可以用下图来表示:

从上图我们可以看出,ip_tables模块它是防火墙的核心模块,负责维护防火墙的规则表,通过这些规则,实现防火墙的核心功能。归纳起来,主要有三种功能:包过滤(filter)、NAT以及包处理(mangle)。同进该模块留有与用户空间通讯的接口。如第一篇博文中Netfilter处于内核中位置那副图所描述的情形。

在内核中我们习惯将上述的filter,nat和mangle等称之为模块。连接跟踪conntrack有些特殊,它是NAT模块和状态防火墙的功能基础,其实现机制我们也会在后面详细分析的。

OK,回到开篇的问题,我们来看一下基于Netfilter的防火墙系统到底定义了哪些钩子函数?而这些钩子函数都是分别挂载在哪些hook点的?按照其功能结构划分,我将这些hook函数总结如下:

包过滤子功能:包过滤一共定义了四个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 内核机制最有效,最直观的方式了,所以屡用不爽!

然后,我们拿一把大刀,从协议栈的IPv4点上顺着hook点延伸的方向一刀切下去,就会得到一个平面,如上图所示。前面这些hook函数在这个平面上的分布情况如下所示:

这幅图彻底暴露了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:

    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不解释

      const struct net_device *in,         //数据包的网络如接口

      const struct net_device *out,       //<span times="" new="" roman";="" mso-hansi-font-family:"times="" roman";mso-bidi-font-family:"times="" mso-font-kerning:0pt"="" style="word-wrap: break-word; font-size: 10pt; font-family: 宋体;">数据包的网络出接口

          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宏仅仅只是一个跳转而已,更重要的内容是“内核是如何注册钩子函数的呢?这些钩子函数又是如何被调用的呢?谁来维护和管理这些钩子函数?”

未完,待续…


作为ipchains的后继者,iptables具有更加优越的特性,良好的可扩展功能、更高的安全性以及更加紧凑、工整、规范的代码风格。

     2.6 的内核中默认维护了三张表 ( 其实是四张,还有一个名为 raw 的表很少被用到,这里不对其进行分析介绍了 ) filter 过滤表, nat 地址转换表和 mangle 数据包修改表,每张表各司其职。

我们对这三张表做一下简要说明:

    1)、filter

    该表是整个过滤系统中真正起“过滤”作用的地方。所有对数据包的过滤工作都在这个表里进行,也就是说用户如果需要对某种类型的数据包进行过滤拦截,那么最好在这个表中进行操作。filter表会在NF_IP_LOCAL_INNF_IP_FORWARDNF_IP_LOCAL_OUT三个hook点注册钩子函数,也就是说所有配置到filer表中的规则只可能在这三个过滤点上进行设置。

    2)、nat

    主要用于DNATSNAT和地址伪装等操作。用于修改数据包的源、目的地址。目前版本的内核中nat表监视四个hook点:NF_IP_PRE_ROUTINGNF_IP_LOCAL_IN/OUTNF_IP_POST_ROUTING。但在真正的实际应用中,我们一般仅需要在nat表的PREROUTINGPOSTROUTING点上注册钩子函数。该表有个特性:只有新连接的第一个数据包会经过这个表,随后该连接的所有数据包将按照第一个数据包的处理动作做同样的操作,这种特性是由连接跟踪机制来实现的。

    3)、mangle

     该表主要用于对数据包的修改,诸如修改数据包的 TOS TTL 等字段。同时该表还会对数据包打上一些特殊的标签以便结合 TC 等工具,实现诸如 Qos 等功能。该表监视所有的 hook 点。

    IT 界有位大牛 ( 具体是哪个我记不太清楚了 ) 曾给程序下的定义是:程序 = 数据结构 + 算法。可见数据结构在整个程序设计过程中的重要性了。我本人也比较赞同这种说法。我们今天主要探究一下通过用户空间的 iptables<span times="" new="" roman";="" mso-hansi-font-family:"times="" roman";mso-bidi-font-family:"times="" mso-ansi-language:en-us;mso-fareast-language:zh-cn;mso-bidi-language:ar-sa"="" style="word-wrap: break-word; font-size: 10.5pt; font-family: 宋体;">所配置到内核中的每条规则到底是个啥样子。

   在 Netfilter 中规则是顺序存储的,一条rule规则主要包括三个部分:

  • ipt_entry:标准匹配结构,主要包含数据包的源、目的IP,出、入接口和掩码等;
  • ipt_entry_match:扩展匹配。一条rule规则可能有零个或多个ipt_entry_match结构;
  • ipt_entry_target:一条rule规则有且仅有一个target动作。就是当所有的标准匹配和扩展匹配都符合之后才来执行该target。
   结构体 struct ipt_entry {} 的定义在 include/linux/netfilter_ipv4/ip_tables.h 文件里。其结构图如下:

上面这几个结构体的成员属性基本上已经做到了“见名知意”,而且内核源码也对它们做了充分的注解。这里只对最后一个属性elem做一下说明,其定义为unsigned char elems[0]。大家可能觉得有些奇怪,怎么定了一个大小为零的数组呢?而且有些面试官曾经就这样的定义还向面试者发问过呢。这种方式定义的数组叫柔性数组,又叫可变长数组。为了不至于冲淡本文主题,这里给出一个关于柔性数组的链接,这位大牛已经将的很清楚了,大家可以去拜读拜读:http://blog.csdn.net/supermegaboy/article/details/4854939。更多详细的内容可以去研读C99标准。

        我们将看到内核中大量的在运用柔性数组,包括我们即将要介绍的这两个结构体:

    这两个双胞胎兄弟一眼望去还以为它们是同一个东西,但事实并非如你所想的那样。其中ipt_entry_match{}表示防火墙规则的匹配部分;ipt_entry_target{}表示防火墙规则的动作处理部分。大家先忽略掉它们左边那条烦人的提示部分union,后面我会详细介绍的,现在请跟我一样尽情地无视它们吧。

    这里我们还注意到,这两个家伙分别都拖了一条小尾巴:ipt_match{}ipt_target{}。对于前面我们提到过的标准匹配,它只会去检查数据包的IP地址,源目的接口,掩码等通用信息,不会用到ipt_entry_match{}。对于以模块形式存在的扩展匹配,如iprange模块,ipp2p模块等,它们就得实现自己的ipt_match{}结构。也就是说,如果你要开发一个新的match模块,那么就必须去实例化一个ipt_match{}结构体对象,并实现该结构体中相应的成员属性(其实主要都是一些些函数指针成员,你必须实现相应的函数体),然后将该ipt_match{}对象挂在你的ipt_entry_match{}结构的match属性里就OK了,就这么简单。

    同样的,我们来说一下ipt_target{}结构体。对于target(我这里就不翻译了,那个叫“动作”的翻译太难听了,后面我都用英文表述)也分为标准target和扩展target。标准target就是那些ACCEPTDROPREJECT等等之类的处理方式;扩展target就是那些诸如DNATSNAT等以模块形式存在的target了。对于标准的target,它是不需要ipt_target{}结构的,即ipt_entry_target{}中的target属性为NULL;而对于我们自己扩展target是需要我们自己手工去实现ipt_target{}对象,并完成相关回调函数的编写。对于ipt_target{}结构体中target回调函数的编写有一点要注意:该函数必须向Netfilter框架返回IPT_CONTINUE、或者诸如NF_ACCEPTNF_DROP之类的值。开发细节我会在后续动手实践章节一一向大家说明。

    结构体ipt_entry_match{}定义在include/linux/netfilter/x_tables.h文件中。

    结构体ipt_entry_target{}也定义在include/linux/netfilter/x_tables.h文件中。

    结构体ipt_match{}定义在include/linux/netfilter/ip_tables.h文件中。

     结构体 ipt_target{} 也定义在 include/linux/netfilter/ip_tables.h 文件中。

匹配match:

    上面我们说过,match分为两种:基本match,又叫标准match;扩展match。

n  标准match:

标准匹配主要用于匹配由struct ipt_ip{}所定义的数据包的特征项。标准匹配的内核数据结构就是我们上面所看到的ipt_match{}定义在include/linux/netfilter/ip_tables.h。在所有的表中我们最后真正所用到的match结构为ipt_entry_match{},它和ipt_match{}的关系我也将其画出来了,如上所示。

既然说到这里,那我就再啰嗦一点。对于ipt_entry_match{}的结构大家可能也留意到了它内部结构有个union成员,同时它还区分了user和kernel两种情况。我们刚刚在上面所讨论的ipt_match{}结构是内核中用来表示match的数据类型,在用户空间我们用的是iptables_match{}结构(定义在iptables.tar.gz源码包中的include/iptables.h头文件中)来表示match的。

也就是说,内核空间和用户空间在注册和维护match时使用的是各自的match结构,ipt_match{}和iptables_match{},但是当某个具体的match被应用到防火墙规则里时,它们两个必须统一成ipt_entry_match{},这才是防火墙规则中真正用到的match结构。

至于iptables_match{}和ipt_match{}是如何完美地统一到ipt_entry_match{}结构中的,我们在后面再来详细分析。

n  扩展match:

扩展match通常以插件或模块的形式存在。当我们在用户空间通过iptables命令设置规则时如果用到了-m  ‘name’ 参数时,那么此时‘name就是一个扩展匹配模块。前面我们也说过,如果你需要开发一个新的match模块,那么就必须去实例化一个ipt_match{}结构体对象,并实现其中的重要回调函数(如match()函数),最后通过xt_register_match()接口将你的ipt_match{}对象注册到Netfilter中去就可以了。实战篇我们讲解如何开发一个新match的全过程。

 

动作target:

         根据上面的两幅图我们可以看到ipt_entry_match{}和ipt_entry_target{}的结构基本如出一辙,那么它们也就存在着很多非常相似的地方了。 在所有的表中关于target的使用,我们既可以用ipt_standard_target{}又可以用ipt_entry_target{},这是为什么呢?后面再解释。这两个结构体的关系如下图所示:

    怎么样很简单吧,就多了一个verdict变量而已。说了半天,那么target到底是用来干什么的呢?说白了,target主要用来处理:当某条规则中的所有match都被数据包匹配后该执行什么样的动作来处理这个报文,最后将处理后结果通过verdict值返回给Netfilter框架

    同样的target也分内核空间和用户空间两种结构。在内核空间中,我们所说的target由ipt_target{}表示,定义在include/linux/netfilter/ip_tables.h文件中;在用户空间中,所使用的是iptables_target{}结构,该结构定义在iptables源码包里的iptables.h头文件中。同样地,ipt_target{}iptables_target{}最后也完美地统一到了ipt_entry_target{}里。

iptables的-j参数后面即可跟诸如ACCEPT、DROP这些动作,也可以跟一条用户自定义链表的名字。我们都知道iptables中的规则链(chain)其实就是某个hook点上所有规则的集合。除了系统内建的链外,用户还可以创建自定义的新链。在iptables中,同一个链里的规则是顺序存放的。内建链的最后一条规则的target是链的policy策略,而用户自定义链中是没有policy这么一说。用户自定义链的最后一条规则的target是NF_RETURN,遍历过程将返回原来的链中。当然,规则中的target也可以指定跳转到某个用户创建的自定义链上,这时这条规则的target就是ipt_standard_target{}类型,并且这个target的verdict值大于0。如果在用户自定义链上没有找到任何匹配的规则的话,遍历过程将返回到原来调用这条用户自定链的链里去匹配下一条规则。

这里还需要注意一点:target也分为标准target扩展target,前面简单提过一些。它和标准的match以及扩展match还是有些区别:

标准的target,即ipt_standard_target{}里可以根据verdict的值再划分为内建的动作或者跳转到自定义链中。简单了说,标准的target就是内核内建的一些处理动作或其延伸。

扩展的target,则完全是由用户定义的处理动作。如果ipt_target.target()函数是空的,那就是标准target,因为它不需要用户再去提供新的target函数了;反之,如果有target函数那就是扩展的target。

    如果我们要开发自己的target,那么也只需要实例化一个ipt_target{}对象,并填充其内部相关的回调函数,然后调用xt_register_target()将其注册到Netfilter框架即可。

 

规则rule:

          其实每张 table 表中最后真正用于表示其内部所有规则的结构体是 ipt_standard{} ,定义在 include/linux/netfilter_ipv4/ip_tables.h 文件中。最后在我们几张表里,如 filter 表, nat 表里真正用的规则结构为 ipt_standard{} 。它和我们前面介绍的 ipt_entry{} 的关系如下:
    未完,待续...

今天我们讨论一下防火墙的数据包过滤模块iptable_filter的设计原理及其实现方式。

    内核中将filter模块被组织成了一个独立的模块<net/ipv4/netfilter/iptable_filter.c>,每个这样独立的模块中都有个类似的init()初始化函数。编写完该函数后,用module_init()宏调用初始化函数;同样当模块被卸载时调用module_exit()宏将该模块卸载掉,该宏主要调用模块的“析构”函数。这当中就牵扯到内核ko模块的一些知识,但这并不妨碍我们理解。

 

    整个filter模块就一百多行代码,但要将其理解清楚还是需要一些功夫。我们首先来看一下filter模块是如何将自己的钩子函数注册到netfilter所管辖的几个hook点的。 

static int __init iptable_filter_init(void)

{

      int ret;

      if (forward < 0 || forward > NF_MAX_VERDICT) {

               printk("iptables forward must be 0 or 1\n");

               return -EINVAL;

      }

      /* Entry 1 is the FORWARD hook */

      initial_table.entries[1].target.verdict = -forward - 1;


      /* Register table */

     ret = ipt_register_table(&packet_filter, &initial_table.repl);

     if (ret < 0)

           return ret;


     /* Register hooks */

     ret = nf_register_hooks(ipt_ops, ARRAY_SIZE(ipt_ops));

     if (ret < 0)

         goto cleanup_table;

     return ret;


 cleanup_table:

     ipt_unregister_table(&packet_filter);

     return ret;

}

    这里我只看关键部分,根据上面的代码我们已经知道。filter模块初始化时先调用ipt_register_table向Netfilter完成filter过滤表的注册,然后调用ipt_register_hooks完成自己钩子函数的注册,就这么简单。至于这两个注册的动作分别都做了哪些东西,我们接下来详细探究一下。

 

注册过滤表:ipt_register_table(&packet_filter, &initial_table.repl);

    Netfilter在内核中为防火墙系统维护了一个结构体,该结构体中存储的是内核中当前可用的所有match,target和table,它们都是以双向链表的形式被组织起来的。这个全局的结构体变量static struct xt_af *xt定义在net/netfilter/x_tables.c当中,其结构为:

struct xt_af {

         struct mutex mutex;

         struct list_head  match; //每个match模块都会被注册到这里

         struct list_head  target; //每个target模块都会被注册到这里

         struct list_head  tables; //每张表都被被注册到这里

         struct mutex compat_mutex;

};

    其中xt变量是在net/netfilter/x_tables.c文件中的xt_init()函数中被分配存储空间并完成初始化的,xt分配的大小以当前内核所能支持的协议簇的数量有关,其代码如下:

    初始化完成后 xt 的结构图如下所示,这里我们只以 IPv4 协议为例加以说明:

    每注册一张表,就会根据该表所属的协议簇,找到其对应的xt[]成员,然后在其tables双向链表中挂上该表结构即完成了表的注册。接下来我们再看一下Netfilter是如何定义内核中所认识的“表”结构的。

    关于表结构,内核中有两个结构体xt_table{}和xt_table_info{}来表示“表”的信息。

    struct ipt_table{}的结构体类型定义在<include/linux/netfilter/x_tables.h>中,它主要定义表自身的一些通用的基本信息,如表名称,所属的协议簇,所影响的hook点等等。

struct xt_table            //其中#define ipt_table xt_table

{

     struct list_head  list;

     char  name[XT_TABLE_MAXNAMELEN];  //表的名字

     unsigned int  valid_hooks;   //该表所检测的HOOK点

     rwlock_t  lock;   //读写锁

     void  *private; //描述表的具体属性,如表的size,表中的规则数等

     struct module  *me; //如果要设计成模块,则为THIS_MODULE;否则为NULL

     int  af;            //协议簇 ,如PF_INET(或PF_INET)

};

而每张表中真正和规则相关的信息,则由该结构的的private属性来指向。从2.6.18版内核开始,该变量被改成了void*类型,目的是方便日后对其进行扩充需要。通常情况下,private都指向一个xt_table_info{}类型的结构体变量。

    struct xt_table_info{}的结构体类型定义在< include/linux/netfilter/x_tables.h >中。 

struct xt_table_info

{

    unsigned int  size; //表的大小,即占用的内存空间

    unsigned int  number; //表中的规则数

    unsigned int  initial_entries; //初始的规则数,用于模块计数

 

    /* 记录所影响的HOOK的规则入口相对于下面的entries变量的偏移量*/

    unsigned int  hook_entry[NF_IP_NUMHOOKS];

    /* 与hook_entry相对应的规则表上限偏移量,当无规则录入时,相应的hook_entry和underflow均为0 */

    unsigned int  underflow[NF_IP_NUMHOOKS];

    char  *entries[NR_CPUS];

};

    该结构描述了表中规则的一些基本信息,同时在该结构的末尾指示了该表中所有规则的入口点,即表中的第一条规则。记住:所有的规则是顺序依次存放的,参见博文三。
    回到前面注册过滤表的地方: ipt_register_table (&packet_filter, &initial_table.repl); 给它传递的第一个参数 packet_filter 就是我们的 filter 表的自身一些信息,仅此而已。

    我们发现ipt_register_table()函数还有一个输入参数:initial_table。根据其名称不难推断出它里面存储的就是我们用于初始化表的一些原始数据,该变量的结构虽然不复杂,但又引入了几个其他的数据结构,如下: 

static  struct

{

    struct ipt_replace  repl;

    struct ipt_standard  entries[3];

    struct ipt_error  term;

} initial_table;

    在注册过滤表时我们只用到了该结构中的struct ipt_replace repl成员,其他成员我们暂时先不介绍,主要来看一下这个repl是个神马东东。

ipt_replace{}结构体的定义在include/linux/netfilter_ipv4/ip_tables.h文件中。其内容如下: 

struct ipt_replace

{

    char  name[IPT_TABLE_MAXNAMELEN];  //表的名字

    unsigned int  valid_hooks;   //所影响的HOOK点

    unsigned int  num_entries;   //表中的规则数目

    unsigned int  size; //新规则所占用存储空间的大小

 

    unsigned int  hook_entry[NF_IP_NUMHOOKS]; //进入HOOK的入口点

    unsigned int  underflow[NF_IP_NUMHOOKS]; /* Underflow points. */

 

    /* 这个结构不同于ipt_table_info之处在于它还要保存旧的规则信息*/

    /* Number of counters (must be equal to current number of entries). */

    unsigned int  num_counters;

    /* The old entries' counters. */

    struct xt_counters __user  *counters;

 

    /* The entries (hang off end: not really an array). */

    struct ipt_entry  entries[0];

};

    之所以要设计ipt_replace{}这个结构体,是因为在1.4.0版的iptables中有规则替换这个功能,它可以用一个新的规则替换掉指定位置上的已存在的现有规则(关于iptables命令行工具的详细用法请参见man手册或iptables指南)。最后我们来看一下initial_table.repl的长相: 

initial_table.repl= { "filter", FILTER_VALID_HOOKS, 4,

       sizeof(struct ipt_standard) * 3 + sizeof(struct ipt_error),

       { [NF_IP_LOCAL_IN] = 0,

         [NF_IP_FORWARD] = sizeof(struct ipt_standard),

         [NF_IP_LOCAL_OUT] = sizeof(struct ipt_standard) * 2

       },

       { [NF_IP_LOCAL_IN] = 0,

         [NF_IP_FORWARD] = sizeof(struct ipt_standard),

         [NF_IP_LOCAL_OUT] = sizeof(struct ipt_standard) * 2

       },

      0, NULL, { }

};

    根据上面的初始化代码,我们就可以弄明白initial_table.repl成员的意思了:

    "filter"表从"FILTER_VALID_HOOKS"这些hook点介入Netfilter框架,并且filter表初始化时有"4"条规则链,每个HOOK点(对应用户空间的“规则链”)初始化成一条链,最后以一条“错误的规则”表示结束,filter表占(sizeof(struct ipt_standard) * 3+sizeof(struct ipt_error))字节的存储空间,每个hook点的入口规则如代码所示。     因为初始化模块时不存在旧的表,因此后面两个个参数依次为0、NULL都表示“空”的意思。最后一个柔性数组struct ipt_entry  entries[0]中保存了默认的那四条规则。

    由此我们可以知道,filter表初始化时其规则的分布如下图所示:

我们继续往下走。什么?你说还有个ipt_error?记性真好,不过请尽情地无视吧,目前讲了也没用。那你还记得我们现在正在讨论的是什么主题吗?忘了吧,我再重申一下:我们目前正在讨论iptables内核中的filter数据包过滤模块是如何被注册到Netfilter中去的!!

有了上面这些基础知识我们再分析ipt_register_table(&packet_filter, &initial_table.repl)函数就容易多了,该函数定义在net/ipv4/netfilter/ip_tables.c中: 

int ipt_register_table(struct xt_table *table, const struct                                                         ipt_replace *repl)

{

    int ret;

    struct xt_table_info *newinfo;

    static struct xt_table_info bootstrap = {

         0, 0, 0, { 0 }, { 0 }, { } 

    };

   void *loc_cpu_entry;

   newinfo = xt_alloc_table_info(repl->size); //为filter表申请存储空间

   if (!newinfo)

       return -ENOMEM;

 

   //将filter表中的规则入口地址赋值给loc_cpu_entry

     loc_cpu_entry = newinfo->entries[raw_smp_processor_id()];

   //将repl中的所有规则,全部拷贝到newinfo->entries[]中

     memcpy(loc_cpu_entry, repl->entries, repl->size);

   /*translate_table函数将由newinfo所表示的table的各个规则进行边界检查,然后对于newinfo所指的xt_talbe_info结构中的hook_entries和underflows赋予正确的值,最后将表项向其他cpu拷贝*/

     ret = translate_table(table->name, table->valid_hooks,

                                  newinfo, loc_cpu_entry, repl->size,

                                  repl->num_entries,

                                  repl->hook_entry,

                                  repl->underflow);

    if (ret != 0) {

           xt_free_table_info(newinfo);

           return ret;

    }

    //这才是真正注册我们filter表的地方

    ret = xt_register_table(table, &bootstrap, newinfo);

    if (ret != 0) {

           xt_free_table_info(newinfo);

           return ret;

    }

    return 0;

}

    在该函数中我们发现点有意思的东西:还记得前面我们在定义packet_filter时是什么情况不? packet_filter中没对其private成员进行初始化,那么这个工作自然而然的就留给了xt_register_table()函数来完成,它也定义在x_tables.c文件中,它主要完成两件事:

    1)、将由newinfo参数所存储的表里面关于规则的基本信息结构体xt_table_info{}变量赋给由table参数所表示的packet_filter{}的private成员变量;

2)、根据packet_filter的协议号af,将filter表挂到变量xt中tables成员变量所表示的双向链表里。

最后我们回顾一下ipt_register_table(&packet_filter, &initial_table.repl)的初始化流程:

    简而言之ipt_register_table()所做的事情就是从模板initial_table变量的repl成员里取出初始化数据,然后申请一块内存并用repl里的值来初始化它,之后将这块内存的首地址赋给packet_filter表的private成员,最后将packet_filter挂载到xt[2].tables的双向链表中。


注册钩子函数:nf_register_hooks(ipt_ops, ARRAY_SIZE(ipt_ops));

    在第二篇博文中我们已经简单了解nf_hook_ops{}结构了,而且我们也知道该结构在整个Netfilter框架中的具有相当重要的作用。当我们要向Netfilter注册我们自己的钩子函数时,一般的思路都是去实例化一个nf_hook_ops{}对象,然后通过nf_register_hook()接口其将其注册到Netfilter中即可。当然filter模块无外乎也是用这种方式来实现自己的吧,那么接下来我们来研究一下filter模块注册钩子函数的流程。

    首先,我们看到它也实例化了一个nf_hook_ops{}对象——ipt_ops,代码如下所示: 

static  struct nf_hook_ops  ipt_ops[] = {

   {

       .hook                  = ipt_hook,

       .owner               = THIS_MODULE,

       .pf              = PF_INET,

       .hooknum         = NF_IP_LOCAL_IN,

       .priority    = NF_IP_PRI_FILTER,

    },

    {

       .hook                  = ipt_hook,

       .owner               = THIS_MODULE,

       .pf              = PF_INET,

       .hooknum         = NF_IP_FORWARD,

       .priority    = NF_IP_PRI_FILTER,

    },

    {

       .hook                  = ipt_local_out_hook,

       .owner               = THIS_MODULE,

       .pf              = PF_INET,

       .hooknum         = NF_IP_LOCAL_OUT,

       .priority    = NF_IP_PRI_FILTER,

    },

};

    对上面这种定义的代码我们现在应该已经很清楚其意义了:iptables的filter包过滤模块在Netfilter框架的NF_IP_LOCAL_IN和NF_IP_FORWARD两个hook点以NF_IP_PRI_FILTER(0)优先级注册了钩子函数ipt_hook(),同时在NF_IP_LOCAL_OUT过滤点也以同样的优先级注册了钩子函数ipt_local_out_hook()

    然后,在nf_register_hooks()函数内部通过循环调用nf_register_hook()接口来完成所有nf_hook_ops{}对象的注册任务。在nf_register_hook()函数里所执行的操作就是一个双向链表的查找和插入,没啥难度。考大家一个问题,测试一下你看博客的认真和专心程度:filter模块所定义的这些hook函数是被注册到哪里去了呢?

 

=================================华丽丽的分割线================================



    想不起的话可以去复习一下第一篇博文结尾部分的内容,不过我知道大多数人都懒的翻回去了。好吧,我再强调一遍:所有的hook函数最终都被注册到一个全局的二维的链表结构体数组struct list_head nf_hooks[NPROTO][NF_MAX_HOOKS]里了。一维表示协议号,二维表示hook点。

    还记得我们给过滤模块所有hook函数所划的分类图么:

    目前只出现了ipt_hook和ipt_local_out_hook,不过这四个函数本质上最后都调用了ipt_do_table()函数,而该函数也是包过滤的核心了。

 

数据包过滤的原理:

    根据前面我们的分析可知,ipt_do_table()函数是最终完成包过滤功能的这一点现在已经非常肯定了,该函数定义在net/ipv4/netfilter/ip_tables.c文件中。实际上,90%的包过滤函数最终都调用了该接口,它可以说是iptables包过滤功能的核心部分。在分析该函数之前,我们把前几章中所有的相关数据结构再梳理一遍,目的是为了在分析该函数时达到心中有数。

    我们前面提到过的核心数据结构有initial_table、ipt_replace、ipt_table、ipt_table_info、

ipt_entry、ipt_standard、ipt_match、ipt_entry_match、ipt_target、ipt_entry_target,这里暂时没有涉及到对用户空间的相应的数据结构的讨论。以上这些数据结构之间的关系如下:

    我们还是先看一下ipt_do_table()函数的整体流程图:

    我们分析一下整个ipt_do_table()函数执行的过程:

    对某个hook点注册的所有钩子函数,当数据包到达该hook点后,该钩子函数便会被激活,从而开始对数据包进行处理。我们说过:规则就是“一组匹配+一个动作”,而一组规则又组成了所谓的“表”,因此,每条规则都属于唯一的一张表。前面我们知道,每张表都对不同的几个HOOK点进行了监听,而且这些表的优先级是不相同的,我们在用户空间里去配置iptables规则的时候恰恰也是必须指定链名和表名,在用户空间HOOK点就被抽象成了“链”的概念,例如:

    iptables  –A  INPUT  –p  tcp  –s  ! 192.168.10.0/24  –j  DROP

    这就表示我们在filter表的NF_IP_LOCAL_IN这个HOOK点上增加了一个过滤规则。当数据包到达LOCAL_IN这个HOOK点时,那么它就有机会被注册在这个点的所有钩子函数处理,按照注册时候的优先级来。因为表在注册时都已确定了优先级,而一个表中可能有数条规则,因此,当数据包到达某个HOOK点后。优先级最高的表(优先级的值越小表示其优先程度越高)中的所有规则被匹配完之后才能轮到下一个次高优先级的表中的所有规则开始匹配(如果数据包还在的话)。

    所以,我们在ipt_do_table()中看到,首先就是要获取表名,因为表名和优先级在某种程度上来说是一致的。获取表之后,紧接着就要获取表中的规则的起始地址。然后用依次按顺序去比较当前正在处理的这个数据包是否和某条规则中的所有过滤项相匹配。如果匹配,就用那条规则里的动作target来处理包,完了之后返回;如果不匹配,当该表中所有的规则都被检查完了之后,该数据包就转入下一个次高优先级的过滤表中去继续执行此操作。依次类推,直到最后包被处理或者被返回到协议栈中继续传输。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值