netfilter机制

Packet flow in netfilter and general networking

blob.png

一个网络通信的基本模型:

blob.png

从上图可以看出数据发送过程是一个加头过程,而数据接收过程是一个剃头过程。

网卡接收的数据包在协议栈中传输过程如下图所示:

blob.png

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

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

协议栈中的五个关键点ABCDE就是研究netfilter的关键。内核中也给了它们头衔,具体如下图所示:

blob.png

每个节点上我们根据需要都可以挂接上我们自定义的hook函数(当然我们可以指定它们的优先级)。

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

blob.png

Struct nf_hook_ops结构

blob.png

hooknum这个成员用于指定安装的这个函数对应的具体的hook类型(即为上述的ABCDE):

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

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

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

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

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

struct nf_hook_ops只是存储勾子的数据结构,而真正存储这些勾子供协议栈调用的是nf_hooks

blob.png

可以看出这是个二维链表,其中NFPROTO_NUMPROTO表示钩子关联协议族,NF_MAX_HOOKS表示钩子应用位置或者说是hook类型。

注册hook函数nf_register_hooks

blob.png

List_for_each函数遍历当前待注册钩子的协议pfhook类型所对应的链表,其首地址为&nf_hooks[reg->pf][reg->hooknum],如果当前hook函数的优先级小于插入节点的优先级,则找到了待插入的位置(说明按优先级的升序排列),再调用list_add_rcu函数将hook函数插入到查找到的合适位置。这样所有pf协议下的hooknum类型的钩子,都被注册到&nf_hooks[reg->pf][reg->hooknum]为首的链表当中了。下面给出nf_hooks的结构图:

blob.png

Hook函数的定义:

blob.png

返回值:

Ø  NF_ACCEPT 继续正常传输数据报。这个返回值告诉 Netfilter:到目前为止,该数据包还是被接受的并且该数据包应当被递交到网络协议栈的下一个阶段。

Ø  NF_DROP 丢弃该数据报,不再传输。

Ø  NF_STOLEN 模块接管该数据报,告诉Netfilter“忘掉该数据报。该回调函数将从此开始对数据包的处理,并且Netfilter应当放弃对该数据包做任何的处理。但是,这并不意味着该数据包的资源已经被释放。这个数据包以及它独自的sk_buff数据结构仍然有效,只是回调函数从Netfilter 获取了该数据包的所有权。

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

Ø  NF_REPEAT 再次调用该回调函数,应当谨慎使用这个值,以免造成死循环。

既然已经注册了hook函数,接下来的疑问就是何时何地被调用,在内核netfilter中定义了一个NF_HOOK的函数(一不小心你会发现还有一个宏(调用okfn函数),当然这没冲突,在编译内核是你可以选择是否编译netfilter模块,若编译则定义NF_HOOK函数,若否,则直接定义该宏):

blob.png

其参数是不是和我们定义hook函数有几分雷同,接下来,我们可以一路跟踪下,查他个水落石出:

blob.png

上面函数中的nf_hook_thresh()okfn()两个调用是不是很吸引眼球,okfn是我们的熟人了(hook函数定义中那个默认处理函数),显然它受nf_hook_thresh函数摆布,通过返回值来命令okfn是执行还是不执行。好吧,既然bossnf_hook_thresh,擒贼先擒王先:

blob.png

看到这,是不是有一点柳暗花明的感觉呢?先判断netfilter hook链是否有挂接的hook函数,若是,接下来就是依优先级依次调用我们的hook了,是这样的吗?nf_hook_slow就是答案:

/* Returns 1 if okfn()   needs to be executed by the caller,

 * -EPERM for NF_DROP, 0 otherwise. */

int nf_hook_slow(u_int8_t pf, unsigned   int hook, struct sk_buff *skb,

               struct net_device *indev,

               struct net_device *outdev,

               int (*okfn)(struct sk_buff *),

               int hook_thresh)

{

       struct   nf_hook_ops *elem;

       unsigned   int verdict;

       int   ret = 0;

 

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

       rcu_read_lock();

/*获得hook链表的头地址*/

       elem   = list_entry_rcu(&nf_hooks[pf][hook], struct nf_hook_ops, list);

next_hook:

       /*开始调用hook函数,nf_iterate函数中直接调用hook*/

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

                          outdev, &elem, okfn, hook_thresh);

       /*下面是返回值处理*/

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

              ret   = 1;  //返回1okfn就得执行

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

              kfree_skb(skb);

              ret   = NF_DROP_GETERR(verdict);

              if   (ret == 0)

                     ret   = -EPERM;

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

              int   err = nf_queue(skb, elem, pf, hook, indev, outdev, okfn,

                                          verdict   >> NF_VERDICT_QBITS);

              if   (err < 0) {

                     if   (err == -ECANCELED)

                            goto   next_hook;

                     if   (err == -ESRCH &&

                        (verdict &   NF_VERDICT_FLAG_QUEUE_BYPASS))

                            goto   next_hook;

                     kfree_skb(skb);

              }

       }

       rcu_read_unlock();

       return   ret;

}

EXPORT_SYMBOL(nf_hook_slow);

此时,是不是一切答案都被ko啦,好像还缺点什么,NF_HOOK这个家伙是哪来的,反正不是石头里蹦出来。回想下上面提到的五个hook点,我们大概能够猜出个所以然来,eg.打开linux源码,在ip_rcv等函数最后我们会发现NF_HOOK熟悉可爱的身影。

一个简单驱动实例(的确这个例子只能说明大材小用了,呵呵):

#include <linux/kernel.h>

#include <linux/module.h>

#include <linux/ip.h>

#include <linux/version.h>

#include <linux/skbuff.h>

#include <linux/netfilter.h>

#include <linux/netfilter_ipv4.h>

#include <linux/moduleparam.h>

#include   <linux/netfilter_ipv4/ip_tables.h>

 

 

MODULE_LICENSE("GPL");

MODULE_AUTHOR("Monkee");

MODULE_DESCRIPTION("My hook   test");

 

static int pktcnt = 0;

//我们自己定义的hook回调函数,丢弃每第5×n(n=1,2,3,4…)ICMP报文。

static unsigned int   myhook_func(unsigned int hooknum, struct sk_buff **skb, const struct   net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *))

{

     const struct iphdr *iph = (struct iphdr *)ip_hdr(skb);

     if(iph->protocol == 1){

        atomic_inc(&pktcnt);

        if(pktcnt%5 == 0){

           printk(KERN_INFO "%d: drop an   ICMP pkt to %u.%u.%u.%u !\n", pktcnt,NIPQUAD(iph->daddr));

           return NF_DROP;

        }

     }

     return NF_ACCEPT;

}

 

static struct nf_hook_ops nfho={

          .hook           =   myhook_func,  //我们自己的hook回调处理函数

          .owner          = THIS_MODULE,

          .pf             = PF_INET,

          .hooknum        =   NF_IP_LOCAL_OUT, //挂载在本地出口处

          .priority       =   NF_IP_PRI_FIRST,  //优先级最高

};

 

static int __init myhook_init(void)

{

      return nf_register_hook(&nfho);

}

 

static void __exit myhook_fini(void)

{

      nf_unregister_hook(&nfho);

}

 

module_init(myhook_init);

module_exit(myhook_fini);