train

一、基本介绍

1.1  Netfilter概述
        
Netfilter/IPTablesLinux2.4.x之后新一代的Linux防火墙机制,是linux内核的一个子系统。Netfilter采用模块化设计,具有良好的可扩充性。其重要工具模块IPTables从用户态的iptables连接到内核态的Netfilter的架构中NetfilterIP协议栈是无缝契合的,并允许使用者对数据报进行过滤、地址转换、处理等操作。

1.2 基本名词解释

1target//规则匹配后的处理方法

一般将target分为两类,一类为标准的target,即下面的宏定义

NF_DROP                丢弃该数据包

NF_ACCEPT            保留该数据包

NF_STOLEN            忘掉该数据包

NF_QUEUE             将该数据包插入到用户空间

NF_REPEAT            再次调用该hook函数

另一类为由模块扩展的target,

REJECT,LOG,ULOG,TOS,DSCP,MARK,REDIRECT,MASQUERADE,NETMAP

2hook

//这个成员用于指定安装的这个函数对应的具体的hook类型:

NF_IP_PRE_ROUTING    在完整性校验之后,选路确定之前

NF_IP_LOCAL_IN        在选路确定之后,且数据包的目的是本地主机

NF_IP_FORWARD        目的地是其它主机地数据包

NF_IP_LOCAL_OUT        来自本机进程的数据包在其离开本地主机的过程中

NF_IP_POST_ROUTING    在数据包离开本地主机“上线”之前

3chain

相同类型的hook的所有操作以优先级升序排列所组成的链表。

4match//匹配方式

1>标准匹配interface,ip address,protocol

2>由模块延伸出来的匹配

tcp协议高级匹配,udp协议高级匹配,MACaddress 匹配,Multiport匹配,匹配包的MARK值,Owner匹配,IP范围匹配,包的状态匹配,AHESP协议的SPI值匹配,pkttype匹配,(MTU)匹配,limit特定包的重复率匹配,recent,特定包重复率匹配,

ip报头中的TOS值匹配,匹配包中的数据内容。

5table

netfilter中规则都存放在此结构中


1.3  主要源代码文件

Linux内核版本:2.4.x以上

Netfilter主文件:net/core/netfilter.c

 Netfilter主头文件:include/linux/netfilter.h

IPv4相关:

                       c文件:net/ipv4/netfilter/*.c

                     头文件:include/linux/netfilter_ipv4.h

                                     include/linux/netfilter_ipv4/*.h

IPv4协议栈主体的部分c文件,特别是与数据报传送过程有关的部分:

                       ip_input.cip_forward.cip_output.cip_fragment.c

1.4 具体功能模块

  1. 数据报过滤模块
  2. 网络地址转换模块(NAT
  3. 数据报修改模块(mangle
  4. 连接跟踪模块(Conntrack
  5. 其它高级功能模

FILTER,该模块的功能是过滤报文,不作任何修改,或者接受,或者拒绝。它在NF_IP_LOCAL_INNF_IP_FORWARDNF_IP_LOCAL_OUT三处注册了钩子函数,也就是说,所有报文都将经过filter模块的处理。

NAT,网络地址转换(Network AddressTranslation),该模块以ConnectionTracking模块为基础,仅对每个连接的第一个报文进行匹配和处理,然后交由ConnectionTracking模块将处理结果应用到该连接之后的所有报文。natNF_IP_PRE_ROUTINGNF_IP_POST_ROUTING注册了钩子函数;如果需要,还可以在NF_IP_LOCAL_INNF_IP_LOCAL_OUT两处注册钩子,提供对本地报文(出/入)的地址转换。nat 仅对报文头的地址信息进行修改,而不修改报文内容,按所修改的部分,nat可分为源NATSNAT)和目的NATDNAT)两类,前者修改第一个报文的源地址部分,而后者则修改第一个报文的目的地址部分。SNAT可用来实现IP伪装,而DNAT则是透明代理的实现基础。

MANGLE,属于可以进行报文内容修改的table,可供修改的报文内容包括MARKTOSTTL等,mangle表的操作函数嵌入在NetfilterNF_IP_PRE_ROUTING,NF_IP_LOCAL_IN,NF_IP_FORWARD,NF_IP_LOCAL_OUT,NF_IP_POST_ROUTING五处。内核编程人员还可以通过注入模块,调用Netfilter的接口函数创建新的iptables

ConnTrack, 连接跟踪用来跟踪和记录连接状态,是netfilter的一部分,也通过在hook点设定操作函数来完成。

二  Netfilter
2.1 Netfilter 介绍

       NetfilterLinux 2.4.x引入的一个子系统,它作为一个通用的、抽象的框架,提供一整套的hook函数的管理机制,使得诸如数据包过滤、网络地址转换(NAT)和基于协议类型的连接跟踪成为了可能。Netfilter主要通过表、链实现规则,可以这么说,Netfilter是表的容器,表是链的容器,链是规则的容器,最终形成对数据报处理规则的实现。Linux 2.6版内核的Netfilter目前支持IPv4IPv6以及DECnet等协议栈,这里我们主要研究IPv4协议。Netfilter在内核中位置如下图所示:
 

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

2.2 hook点

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

 
        
对于收到的每个数据包,都从“A”点进来,经过路由判决,如果是发送给本机的就经过“B”点,然后往协议栈的上层继续传递;否则,如果该数据包的目的地是不本机,那么就经过“C”点,然后顺着“E”点将该包转发出去。
       
对于发送的每个数据包,首先也有一个路由判决,以确定该包是从哪个接口出去,然后经过“D”点,最后也是顺着“E”点将该包发送出去。
       Netfilter
netfilter_ipv4.h中将这个五个点重新命了个名,这五个关键点我们就叫它们为hook,如下图所示:
 

        
在每个hook点上,有很多已经按照优先级预先注册了的回调函数(称为“钩子函数”),形成了一条链。对于每个到来的数据包会依次被那些回调函数“操作”一番再视情况是将其放行,。但是无论如何,这些钩子函数最后必须向Netfilter报告一下该数据包的情况,每个钩子函数最后必须向Netfilter框架返回下列几个值其中之一:

#define NF_DROP 0         //丢弃数据包,不再传输

#define NF_ACCEPT 1      //允许数据包通行,交由下一个hook处理

#define NF_STOLEN 2   //数据包被送到上层协议处理,不在做netfilter

#define NF_QUEUE 3       //对该数据报进行排队(通常用于将数据报给用户空间的进程进行处理)

#define NF_REPEAT 4       //重复执行当前hook

#define NF_STOP 5           //允许数据包通行,但不再执行以后的hook

#define NF_MAX_VERDICT NF_STOP

       这里面的NF_STOLEN不是很好理解,其实NF_DROP和NF_STOLEN的处理方式大致形同,都代表被阻止,不在被后面的流程发送。但NF_DROP会将skb使用的内存释放掉,以后都不能再处理skb了。而NF_STOLEN表示skb被hook函数处理,由hook函数决定skb是否发送,内存什么时候释放。一般是skb被修改时候使用这个返回值。此时skb的所有者变成了hook函数,原流程后面的函数将不再处理skb,netfilter决定“遗忘”skb,不再去管了。

       另外,NF_REPEAT的使用要十分小心,因为同一个hook函数被反复执行,如果没有合适的结束机制,内核可能在这里死锁。

2.3 优先级
     关于协议类型,hook点,hook函数,优先级,通过下面这个图给大家做个详细展示:
 

         
对于每种类型的协议,数据包都会依次按照hook点的方向进行传输,每个hook点上Netfilter又按照优先级挂了很多hook函数,这些hook函数就是用来处理数据包用的。

netfilter中不同的表在不同的hook点的优先级如下所示:

NF_IP_PRI_FIRST =INT_MIN,  //优先级最高

       NF_IP_PRI_CONNTRACK_DEFRAG= -400,//连接跟踪

       NF_IP_PRI_RAW= -300,//RAW表的优先级

       NF_IP_PRI_SELINUX_FIRST= -225,

       NF_IP_PRI_CONNTRACK= -200,//

       NF_IP_PRI_MANGLE= -150,//

       NF_IP_PRI_NAT_DST= -100,//目标NATDNAT)优先级

       NF_IP_PRI_FILTER= 0,//FILTER表优先级

       NF_IP_PRI_SECURITY= 50,

       NF_IP_PRI_NAT_SRC= 100,//NAT优先级

       NF_IP_PRI_SELINUX_LAST= 225,

       NF_IP_PRI_CONNTRACK_CONFIRM= INT_MAX,//确认CONNTRACK优先级

       NF_IP_PRI_LAST= INT_MAX,

简单说来,进入内核的数据包在经过不同的hook点时会遇到不同的处理机制,Netfilter各个不同的表中规则对数据包进行处理。

2.4 全局变量的注册

所谓的注册其本质上就是将数据存储到全局变量中,为后续的调用做好准备,Netfilter的注册机制可以分为表注册,target注册,match注册,hook操作注册。

2.4.1 表、target注册、match注册

表注册就是将定义好的表存放到一个全局变量xttables成员变量中,这个成员变量为一个xt_af的结构体,定义如下:

//这是一个存放所有规则信息及表信息的数据结构

struct xt_af {

       structmutex mutex;

       structlist_head match;//所有在内核中注册的match信息

       structlist_head target;//所有在内核中注册的target信息

       structlist_head tables;//所有表的信息

       structmutex compat_mutex;

};

static struct xt_af*xt;//一个全局变量,存储了所有内核和用户空间需要的规则信息。    

与表注册类似,target注册和match注册也是通过向全局变量xt的成员变量写入信息来完成的。Netfilter通过这种注册的方式来实现对其扩展机制,用户可以根据自己的需求来实现matchtarget,甚至是自己实现一个表,然后注册到相应的全局变量,当数据包进入Netfilter后,hook操作会查找对应的表,以实现对数据包的匹配和处理。

对应注册接口(/linux/net/netfilter/x_table.c):

/* Registration hooks for targets. */
int xt_register_target(struct xt_target *target)
{
	u_int8_t af = target->family;
	int ret;

	ret = mutex_lock_interruptible(&xt[af].mutex);
	if (ret != 0)
		return ret;
	list_add(&target->list, &xt[af].target);
	mutex_unlock(&xt[af].mutex);
	return ret;
}
EXPORT_SYMBOL(xt_register_target);

<pre name="code" class="cpp" style="color: rgb(51, 51, 51); font-size: 14px; line-height: 21px; text-indent: 28px;">

 
 
<pre name="code" class="cpp" style="color: rgb(51, 51, 51); font-size: 14px; line-height: 21px; text-indent: 28px;">/* Registration hooks for match. */
int xt_register_match(struct xt_match *match){u_int8_t af = match->family;int ret;ret = mutex_lock_interruptible(&xt[af].mutex);if (ret != 0)return ret;list_add(&match->list, &xt[af].match);mutex_unlock(&xt[af].mutex);return ret;}EXPORT_SYMBOL(xt_register_match);
 
 

2.4.2 Hook注册

Hook操作的注册的地点与上面三个注册不同。先来看下什么是hook操作,Netfilter在不同的挂载点注册不同的操作函数,以达到为不同协议的不同挂载点的报文进行不同处理的目的,为实现这个目的Netfilter定义了一个叫做nf_hook_ops(include/linux/netfilter.h)的结构体,具体定义如下:

struct nf_hook_ops

{

       structlist_head list;  //因为在一个HOOK点可能注册多个钩子函数,该变量用来将某个HOOK点所注册的所有钩子函数组织成一个双向链表;

       nf_hookfn*hook;//函数指针,操作的具体执行者

       structmodule *owner;//这个hook属于那个模块

       int pf;//协议族

       int  hooknum;//hook的类型     目前我们主要处理IPv4,该参数总是PF_INET 

       intpriority;//该操作的优先级,插入时使用

};

其中hook成员变量的原型为:

typedef unsigned int nf_hookfn(unsigned int hooknum,

                             structsk_buff *skb,

                            conststruct net_device *in,

                            conststruct net_device *out,

                            int(*okfn)(struct sk_buff *));//hook函数原型

全局 nf_hooks和Hook注册接口(/linux/net/netfilter/core.c):

struct list_head nf_hooks[NPROTO][NF_MAX_HOOKS];//存放所有协议的hooks
    nf_hooks定义了一个二维的结构体数组,用来存储不同协议栈钩子点的回调处理函数,其中,行数NPROTO为32,即目前内核所支持的最大协议簇;列数NF_MAX_HOOKS为挂载点的个数,目前在2.6内核中该值为8,按照priority值的大小从小到大排列。

    nf_hooks数组的最终结构如下图所示。
 
在include/linux/socket.h中IP协议AF_INET(PF_INET)的序号为2,因此我们就可以得到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]


int nf_register_hook(struct nf_hook_ops *reg)
{
	struct nf_hook_ops *elem;
	int err;

	err = mutex_lock_interruptible(&nf_hook_mutex);
	if (err < 0)
		return err;
	list_for_each_entry(elem, &nf_hooks[reg->pf][reg->hooknum], list) {
		if (reg->priority < elem->priority)
			break;
	}
	list_add_rcu(&reg->list, elem->list.prev);
	mutex_unlock(&nf_hook_mutex);
#if defined(CONFIG_JUMP_LABEL)
	static_key_slow_inc(&nf_hooks_needed[reg->pf][reg->hooknum]);
#endif
	return 0;
}
EXPORT_SYMBOL(nf_register_hook);
举例:

static struct nf_hook_ops ipv4_defrag_ops[] = {
	{
		.hook		= ipv4_conntrack_defrag,
		.owner		= THIS_MODULE,
		.pf		= NFPROTO_IPV4,
		.hooknum	= NF_INET_PRE_ROUTING,
		.priority	= NF_IP_PRI_CONNTRACK_DEFRAG,
	},
	{
		.hook           = ipv4_conntrack_defrag,
		.owner          = THIS_MODULE,
		.pf             = NFPROTO_IPV4,
		.hooknum        = NF_INET_LOCAL_OUT,
		.priority       = NF_IP_PRI_CONNTRACK_DEFRAG,
	},
};
nf_register_hooks(ipv4_defrag_ops, ARRAY_SIZE(ipv4_defrag_ops));

2.4.3  netfilter 的内部的整个数据和扩展流程:

Netfilter源码分析--3、Netfilter内部处理流程

     Netfilter将不同协议的不同挂载点的操作函数都存放在一个全局变量nf_hooks中,数据包进入Netfilter后,会查找相应的hook操作。


2.5 协议栈切入Netfilter框架
    
    Netfilter框架之所以能实现许多强大的功能,是因为它在内核若干网络转发的关键函数,设计了许多巧妙的钩子函数,比如数据转发,由两个主要函数A 和B函数实现,流程为A->B ,现在改变为A->钩子函数->B。最常用的钩子函数就是NF_HOOK,这是一个宏定义。Netfilter使用NF_HOOK(include/linux/netfilter.h)宏在协议栈内部切入到Netfilter框架中。
进入 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_ROUTINGLOCAL_INFORWARDLOCAL_OUTPOST_ROUTING会根据数据包的协议簇PF_INET到这些关键点去查找是否注册有钩子函数。如果没有,则直接返回okfn函数指针所指向的函数继续走协议栈;如果有,则调用nf_hook_slow函数,从而进入到Netfilter框架中去进一步调用已注册在该过滤点下的钩子函数,再根据其返回值来确定是否继续执行由函数指针okfn所指向的函数。 

     

当内核编译时CONFIG_NETFILTER宏定义时,即启用netfilter时,代码如下:

#define NF_HOOK(pf, hook, skb, indev, outdev, okfn)\

         NF_HOOK_THRESH(pf,hook, skb, indev, outdev, okfn, INT_MIN)

       NF_HOOK实际上调用了NF_HOOK_THRESH。NF_HOOK_THRESH也是一个宏定义,我们发现NF_HOOK_THRESH宏只增加了一个thresh参数,这个参数就是用来指定通过该宏去遍历钩子函数时的优先级,同时,该宏内部又调用了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函数的参数:

pf

网络协议编号

hook

所属hook点,HOOK点的名字,对于IP层,就是取上面的五个值;

pskb

要处理的数据包,网络设备数据缓存区

indev

数据包进入的接口

outdev

数据包发出的接口

okfn

Netfilter处理完后要运行的函数

thresh

优先级过滤,只有thresh小于优先级的hook函数才能调用

cond

条件判断,当cond为0时直接当NF_ACCEPT处理



还有一个宏定义NF_HOOK_COND。

#define NF_HOOK_COND(pf, hook, skb, indev, outdev,okfn, cond)                         \

({int __ret;                                                                                \

if ((__ret=nf_hook_thresh(pf, hook, &(skb),indev, outdev, okfn, INT_MIN, cond)) == 1)\

         __ret= (okfn)(skb);                                                       \

__ret;})

       由上面的带码,我们可以看到。实际上所有的钩子函数都调用了nf_hook_thresh。当nf_hook_thresh返回值是1是,调用okfn处理skb。

       还有一个函数也调用了nf_hook_thresh,那就是nf_hook。

static inline int nf_hook(int pf, unsigned inthook, struct sk_buff **pskb,

                              struct net_device *indev, struct net_device*outdev,

                              int (*okfn)(struct sk_buff *))

{

         returnnf_hook_thresh(pf, hook, pskb, indev, outdev, okfn, INT_MIN, 1);

}

 

       当没有使用netfilter时,这几个钩子就比较简单了。

#define NF_HOOK(pf, hook, skb, indev, outdev, okfn)(okfn)(skb)

#define NF_HOOK_COND(pf, hook, skb, indev, outdev,okfn, cond) (okfn)(skb)

static inline int nf_hook_thresh(int pf, unsignedint hook,

                                      struct sk_buff **pskb,

                                      struct net_device *indev,

                                      struct net_device *outdev,

                                      int (*okfn)(struct sk_buff *), int thresh,

                                      int cond)

{

         returnokfn(*pskb);

}

static inline int nf_hook(int pf, unsigned inthook, struct sk_buff **pskb,

                              struct net_device *indev, struct net_device*outdev,

                              int (*okfn)(struct sk_buff *))

{

         return1;

}

       当不使用netfilter,nf_hook简单的返回1。而其他钩子函数调用okfn(*pskb)。Okfn就是前面提到的B函数,pskb是要处理的数据包。由此可见,此时的netfilter退化为简单调用okfn。比如在ip_input.c文件中:

return NF_HOOK(PF_INET, NF_IP_LOCAL_IN, skb,skb->dev, NULL,

                          ip_local_deliver_finish);

       实际的效果就是ip_local_deliver_finish(skb)。

 

       这里我们详细分析一下nf_hook_thresh和相关函数。

static inline int nf_hook_thresh(int pf, unsignedint hook,

                                      struct sk_buff **pskb,

                                      struct net_device *indev,

                                      struct net_device *outdev,

                                      int (*okfn)(struct sk_buff *), int thresh,

                                      int cond)

{

         if(!cond)

                   return1;

#ifndef CONFIG_NETFILTER_DEBUG

         if(list_empty(&nf_hooks[pf][hook]))

                   return1;

#endif

         returnnf_hook_slow(pf, hook, pskb, indev, outdev, okfn, thresh);

}

       在这个函数里我们可以看到cond参数的用途,当cond值为0时,不做任何netfilter处理直接返回1.

       nf_hook_thresh最后调用的是nf_hook_slow函数。

int nf_hook_slow(int pf, unsigned int hook, structsk_buff **pskb,

                    struct net_device *indev,

                    struct net_device *outdev,

                    int (*okfn)(struct sk_buff *),

                    int hook_thresh)

{

         structlist_head *elem;

         unsignedint verdict;

         intret = 0;

 

         /* Wemay already have this, but read-locks nest anyway */

         rcu_read_lock();

 

         elem= &nf_hooks[pf][hook];

next_hook:

         verdict= nf_iterate(&nf_hooks[pf][hook], pskb, hook, indev,

                                 outdev, &elem, okfn, hook_thresh);  //执行netfilter的处理函数

         if(verdict == NF_ACCEPT || verdict == NF_STOP) {

                   ret= 1;  //当netfilter的返回值为NF_ACCEPT和NF_STOP直接通过

                   gotounlock;

         }else if (verdict == NF_DROP) {

                   kfree_skb(*pskb);//当netfilter的返回值为NF_DROP,将数据包丢弃

                   ret= -EPERM;

         }else if ((verdict & NF_VERDICT_MASK) == NF_QUEUE) {

                   //当返回值是NF_QUEUE,将数据包插入ip_queue队列,插入的队列的数据包

                   可以用netlink接口从用户态读出。可以实现用户态防火墙

                   NFDEBUG("nf_hook:Verdict = QUEUE.\n");

                   if(!nf_queue(pskb, elem, pf, hook, indev, outdev, okfn,

                                  verdict >> NF_VERDICT_BITS))

                            gotonext_hook;

         }

unlock:

         rcu_read_unlock();

         returnret;

}

       由代码我们可以看到,nf_hook_slow调用nf_iterate来对每个协议的每个hook点的函数处理。

unsigned int nf_iterate(struct list_head *head,

                            structsk_buff **skb,

                            inthook,

                            conststruct net_device *indev,

                            conststruct net_device *outdev,

                            structlist_head **i,

                            int(*okfn)(struct sk_buff *),

                            inthook_thresh)

{

         unsignedint verdict;

         /*

          * The caller must not block between calls tothis

          * function because of risk of continuing fromdeleted element.

          */

         list_for_each_continue_rcu(*i,head) {

                   structnf_hook_ops *elem = (struct nf_hook_ops *)*i;

                   if(hook_thresh > elem->priority)

                            continue;

 

                   /*Optimization: we don't need to hold module

                  reference here, since function can't sleep. --RR */

                   verdict= elem->hook(hook, skb, indev, outdev, okfn); //调用了注册的hook函数

                   if(verdict != NF_ACCEPT) {

#ifdef CONFIG_NETFILTER_DEBUG

                            if(unlikely((verdict & NF_VERDICT_MASK)

                                                                 >NF_MAX_VERDICT)) {

                                     NFDEBUG("Evilreturn from %p(%u).\n",

                                             elem->hook, hook);

                                     continue;

                            }

#endif

                            if(verdict != NF_REPEAT){

                                     IPV4_DEBUG_OUT(IPV4_DEBUG_NFCORE,"NF-CORE:hook= %d, elem->pri = %d verdict = %d\n",

                                                        hook,elem->priority, verdict);

                                     IPV4_DEBUG_OUT(IPV4_DEBUG_NFCORE,"        indev = %s, outdev = %s\n",

                                                        indev?indev->name:"NULL", outdev?outdev->name:"NULL");

                                     returnverdict;

                            }

                            *i= (*i)->prev;  //当返回值是NF_REPEAT,重新执行hook

                   }

         }

         returnNF_ACCEPT;

}

       由代码可以看出,hook_thresh的作用是一个优先级过滤,只有优先级小于elem->priority的hook才能执行。


2.7 netfilter的装载和卸载

自定义hook的装载机卸载,init_module()和cleanup_module,编译出.o(test)文件,使用insmod  test ,insmod会自动查找.o文件中定义的init_module,进行自定义模块的加载,执行rmmod  test,rmmod命令会自动执行模块中的cleanup_module.


三、数据传输流程

3.1 内核中注册的hook回调函数

    数据包在协议栈中传递时会经过不同的HOOK点,而每个HOOK点上又被Netfilter预先注册了一系列hook回调函数,当每个数据包到达这些点后会被这些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点上。

3.2 Hook函数调用流程
    数据报从进入系统,进行 IP 校验以后,首先经过第一个 HOOK 函数 NF_IP_PRE_ROUTING 进行处理;然后就进入路由代码,其决定该数据报是需要转发还是发给本机的;若该数据报是发被本机的,则该数据经过 HOOK 函数 NF_IP_LOCAL_IN 处理以后然后传递给上层协议;若该数据报应该被转发则它被 NF_IP_FORWARD 处理;经过转发的数据报经过最后一个 HOOK 函数 NF_IP_POST_ROUTING 处理以后,再传输到网络上。本地产生的数据经过 HOOK 函数 NF_IP_LOCAL_OUT  处理后,进行路由选择处理,然后经过 NF_IP_POST_ROUTING 处理后发送出去。
  • NF_IP_PRE_ROUTING (0)

    数据报在进入路由代码被处理之前,数据报在IP数据报接收函数ip_rcv()(位于net/ipv4/ip_input.cLine379)的最后,也就是在传入的数据报被处理之前经过这个HOOK。在ip_rcv()中挂接这个HOOK之前,进行的是一些与类型、长度、版本有关的检查。

    经过这个HOOK处理之后,数据报进入ip_rcv_finish()(位于net/ipv4/ip_input.cLine306),进行查路由表的工作,并判断该数据报是发给本地机器还是进行转发。

    在这个HOOK上主要是对数据报作报头检测处理,以捕获异常情况。

涉及功能(优先级顺序):Conntrack(-200)mangle(-150)DNAT(-100)

  • NF_IP_LOCAL_IN (1)

    目的地为本地主机的数据报在IP数据报本地投递函数ip_local_deliver()(位于net/ipv4/ip_input.cLine290)的最后经过这个HOOK

    经过这个HOOK处理之后,数据报进入ip_local_deliver_finish()(位于net/ipv4/ip_input.cLine219

    这样,IPTables模块就可以利用这个HOOK对应的INPUT规则链表来对数据报进行规则匹配的筛选了。防火墙一般建立在这个HOOK上。

涉及功能:mangle(-150)filter(0)SNAT(100)Conntrack(INT_MAX-1)

  • NF_IP_FORWARD (2)

    目的地非本地主机的数据报,包括被NAT修改过地址的数据报,都要在IP数据报转发函数ip_forward()(位于net/ipv4/ip_forward.cLine73)的最后经过这个HOOK

    经过这个HOOK处理之后,数据报进入ip_forward_finish()(位于net/ipv4/ip_forward.cLine44

    另外,在net/ipv4/ipmr.c中的ipmr_queue_xmit()函数(Line1119)最后也会经过这个HOOK。(ipmr为多播相关,估计是在需要通过路由转发多播数据时的处理)

    这样,IPTables模块就可以利用这个HOOK对应的FORWARD规则链表来对数据报进行规则匹配的筛选了。

涉及功能:mangle(-150)filter(0)

  • NF_IP_LOCAL_OUT (3)

    本地主机发出的数据报在IP数据报构建/发送函数ip_queue_xmit()(位于net/ipv4/ip_output.cLine339)、以及ip_build_and_send_pkt()(位于net/ipv4/ip_output.cLine122)的最后经过这个HOOK。(在数据报处理中,前者最为常用,后者用于那些不传输有效数据的SYN/ACK包)

    经过这个HOOK处理后,数据报进入ip_queue_xmit2()(位于net/ipv4/ip_output.cLine281

    另外,在ip_build_xmit_slow()(位于net/ipv4/ip_output.cLine429)和ip_build_xmit()(位于net/ipv4/ip_output.cLine638)中用于进行错误检测;在igmp_send_report()(位于net/ipv4/igmp.cLine195)的最后也经过了这个HOOK,进行多播时相关的处理。

    这样,IPTables模块就可以利用这个HOOK对应的OUTPUT规则链表来对数据报进行规则匹配的筛选了。

涉及功能:Conntrack(-200)mangle(-150)DNAT(-100)filter(0)

  • NF_IP_POST_ROUTING (4)

    所有数据报,包括源地址为本地主机和非本地主机的,在通过网络设备离开本地主机之前,在IP数据报发送函数ip_finish_output()(位于net/ipv4/ip_output.cLine184)的最后经过这个HOOK

    经过这个HOOK处理后,数据报进入ip_finish_output2()(位于net/ipv4/ip_output.cLine160)另外,在函数ip_mc_output()(位于net/ipv4/ip_output.cLine195)中在克隆新的网络缓存skb时,也经过了这个HOOK进行处理。

涉及功能:mangle(-150)SNAT(100)Conntrack(INT_MAX)

四、Netfilter内部处理流程
4.1 Hook操作
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)
/*
 * 	Main IP Receive routine.
 */
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)
{
.....
<span style="white-space:pre">	</span>return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,
<span style="white-space:pre">		</span>       ip_rcv_finish);
csum_error:
<span style="white-space:pre">	</span>IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_CSUMERRORS);
inhdr_error:
<span style="white-space:pre">	</span>IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INHDRERRORS);
drop:
<span style="white-space:pre">	</span>kfree_skb(skb);
out:
<span style="white-space:pre">	</span>return NET_RX_DROP;
}


根据前面的理解,这句代码意义已经很直观明确了。那就是:如果协议栈当前收到了一个 IP 报文 (PF_INET) ,那么就把这个报文传到 Netfilter NF_IP_PRE_ROUTING 过滤点,去检查在那个过滤点 (nf_hooks[2][0]) 是否已经有人注册了相关的用于处理数据包的钩子函数。如果有,则挨个遍历链表 nf_hooks[2][0] 去寻找匹配的 match 和相应的 target ,根据返回到 Netfilter 框架中的值来进一步决定该如何处理该数据包 ( 由钩子模块处理还是交由 ip_rcv_finish 函数继续处理 ) 。刚才说到所谓的“检查”。其核心就是 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);
<pre name="code" class="cpp">int ip_forward(struct sk_buff *skb)
{
	struct iphdr *iph;	/* Our header */
	struct rtable *rt;	/* Route we use */
	struct ip_options *opt	= &(IPCB(skb)->opt);

...........................
 
 
<span style="white-space:pre">	</span>return NF_HOOK(NFPROTO_IPV4, NF_INET_FORWARD, skb, skb->dev,
<span style="white-space:pre">		</span>       rt->dst.dev, ip_forward_finish);

sr_failed:
<span style="white-space:pre">	</span> icmp_send(skb, ICMP_DEST_UNREACH, ICMP_SR_FAILED, 0);
<span style="white-space:pre">	</span> goto drop;
too_many_hops:
<span style="white-space:pre">	</span>/* Tell the sender its packet died... */
<span style="white-space:pre">	</span>IP_INC_STATS_BH(dev_net(skb_dst(skb)->dev), IPSTATS_MIB_INHDRERRORS);
<span style="white-space:pre">	</span>icmp_send(skb, ICMP_TIME_EXCEEDED, ICMP_EXC_TTL, 0);
drop:
<span style="white-space:pre">	</span>kfree_skb(skb);
<span style="white-space:pre">	</span>return NET_RX_DROP;
}
在经过路由抉择后,所有需要本机转发的报文都会交由 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));
int ip_output(struct sk_buff *skb)
{
	struct net_device *dev = skb_dst(skb)->dev;

	IP_UPD_PO_STATS(dev_net(dev), IPSTATS_MIB_OUT, skb->len);

	skb->dev = dev;
	skb->protocol = htons(ETH_P_IP);

	return NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING, skb, NULL, dev,
			    ip_finish_output,
			    !(IPCB(skb)->flags & IPSKB_REROUTED));
}
EXPORT_SYMBOL(ip_output); // 

这里我们看到切入点从无条件宏 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);
/*
 * 	Deliver IP Packets to the higher protocol layers.
 */
int ip_local_deliver(struct sk_buff *skb)
{
	/*
	 *	Reassemble IP fragments.
	 */

	if (ip_is_fragment(ip_hdr(skb))) {
		if (ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER))
			return 0;
	}

	return NF_HOOK(NFPROTO_IPV4, NF_INET_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 报文,然后发送出去。

4.2  IP数据包处理流程

要想理解Netfilter的工作原理,必须从对Linux IP报文处理流程的分析开始,Netfilter正是将自己紧密地构建在这一流程之中的。IP 协议栈是Linux操作系统的主要组成部分,也是Linux的特色之一,素以高效稳定著称。NetfilterIP协议栈是密切结合在一起的,我们以接收报文为例,简要介绍一下IPv4协议栈(IP层)的报文处理过程。

报文接收从网卡驱动程序开始,当网卡收到一个报文时,会产生一个中断,其驱动程序中的中断服务程序将调用确定的接收函数来处理。流程分成两个阶段:驱动程序中断服务程序阶段和IP协议栈处理阶段,驱动程序的处理流程与本章的联系不是十分紧密,故不做详细的介绍,我们以下面的流程图简要介绍以下数据包的接收过程。

驱动程序处理报文时,会生成一个skb_buff,同时将其放入一个全局的存储结构当中,同时设置软中断NET_RX_SOFTIRQ等待内核处理,内核收到软中断后,报文便开始了协议栈之旅。我们用以下的流程图来表示接收报文时整个处理的流程:

从下图的流程可以看出,NetfilterNF_HOOK形式挂载到ip协议栈对报文的处理过程中,然后将相应的数据包转入到Netfilter中来处理,我们可以将IP协议栈中调用NF_HOOK的地方称之为挂载点。除了流程图中指出的几处挂载点,Netfitler还在ip协议栈的多处进行了挂载,可以具体参考内核网络部分的源码。

Netfilter源码分析--3、Netfilter内部处理流程



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值