深入Linux网络核心堆栈(对于netfilter的用法和讲解)

来源:互联网 发布:mac视频下载工具 编辑:程序博客网 时间:2024/06/04 23:46
filter 概述 
         Netfilter/IPTables 是 Linux2.4.x 之后新一代的 Linux 防火墙机制,是linux内核的一个子系统。 Netfilter 采用模块化设计,具有良好的可扩充性。其重要工具模块 IPTables 从用户态的 iptables 连接 到内核态的 Netfilter 的架构中 , Netfilter 与 IP 协议栈是无缝契合的, 并允许使用者对数据报进行过滤、地址转换、处理等操作。 
主要源代码文件 
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.c , ip_forward.c , ip_output.c , ip_fragment.c 等 

具体功能模块

数据报过滤模块 

连接跟踪模块( Conntrack ) 
网络地址转换模块( NAT ) 
数据报修改模块( mangle ) 
其它高级功能模块 

Netfilter 实现 
        Netfilter 主要通过表、链实现规则,可以这么说, 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 在这其中所扮演的角色。 
Netfilter 在 netfilter_ipv4.h 中将这个五个点重新命了个名,如下图所示: 
   
         在每个关键点上,有很多已经按照优先级预先注册了的回调函数 ( 称为“钩子函数” ) 埋伏在这些关键点,形成了一条链。对于每个到来的数据包会依次被那些回调函数“调戏”一番再视情况是将其放行,丢弃还是怎么滴。但是无论如何,这些回调函数最后必须向 Netfilter 报告一下该数据包的死活情况,因为毕竟每个数据包都是 Netfilter 从人家协议栈那儿借调过来给兄弟们 Happy 的,别个再怎么滴也总得“活要见人,死要见尸”吧。每个钩子函数最后必须向 Netfilter 框架返回下列几个值其中之一: 
1. NF_ACCEPT 继续正常传输数据报。这个返回值告诉 Netfilter :到目前为止,该数据包还是被接受的并且该数据包应当被递交到网络协议栈的下一个阶段。 
2. NF_DROP 丢弃该数据报,不再传输。 
3. NF_STOLEN 模块接管该数据报,告诉 Netfilter “忘掉”该数据报。该回调函数将从此开始对数据包的处理,并且 Netfilter 应当放弃对该数据包做任何的处理。但是,这并不意味着该数据包的资源已经被释放。这个数据包以及它独自的 sk_buff 数据结构仍然有效,只是回调函数从 Netfilter 获取了该数据包的所有权。 
4. NF_QUEUE 对该数据报进行排队 ( 通常用于将数据报给用户空间的进程进行处理 ) 
5. 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)2 
关于宏 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, 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) 
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] 

简单地说,数据报经过各个 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.c , Line379 )的最后,也就是在传入的数据报被处理之前经过这个 HOOK 。在 ip_rcv() 中挂接这个 HOOK 之前,进行的是一些与类型、长度、版本有关的检查。 
    经过这个 HOOK 处理之后,数据报进入 ip_rcv_finish() (位于 net/ipv4/ip_input.c , Line306 ),进行查路由表的工作,并判断该数据报是发给本地机器还是进行转发。 
    在这个 HOOK 上主要是对数据报作报头检测处理,以捕获异常情况。 
涉及功能(优先级顺序): Conntrack(-200) 、 mangle(-150) 、 DNAT(-100) 
NF_IP_LOCAL_IN (1) 
    目的地为本地主机的数据报在 IP 数据报本地投递函数 ip_local_deliver() (位于 net/ipv4/ip_input.c , Line290 )的最后经过这个 HOOK 。 
    经过这个 HOOK 处理之后,数据报进入 ip_local_deliver_finish() (位于 net/ipv4/ip_input.c , Line219 ) 
    这样, 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.c , Line73 )的最后经过这个 HOOK 。 
    经过这个 HOOK 处理之后,数据报进入 ip_forward_finish() (位于 net/ipv4/ip_forward.c , Line44 ) 
    另外,在 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.c , Line339 )、以及 ip_build_and_send_pkt() (位于 net/ipv4/ip_output.c , Line122 )的最后经过这个 HOOK 。(在数据报处理中,前者最为常用,后者用于那些不传输有效数据的 SYN/ACK 包) 
    经过这个 HOOK 处理后,数据报进入 ip_queue_xmit2() (位于 net/ipv4/ip_output.c , Line281 ) 
    另外,在 ip_build_xmit_slow() (位于 net/ipv4/ip_output.c , Line429 )和 ip_build_xmit() (位于 net/ipv4/ip_output.c , Line638 )中用于进行错误检测;在 igmp_send_report() (位于 net/ipv4/igmp.c , Line195 )的最后也经过了这个 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.c , Line184 )的最后经过这个 HOOK 。 
    经过这个 HOOK 处理后,数据报进入 ip_finish_output2() (位于 net/ipv4/ip_output.c , Line160 )另外,在函数 ip_mc_output() (位于 net/ipv4/ip_output.c , Line195 )中在克隆新的网络缓存 skb 时,也经过了这个 HOOK 进行处理。 
涉及功能: mangle(-150) 、 SNAT(100) 、 Conntrack(INT_MAX) 
同时我们看到,在 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 过滤点,去检查在那个过滤点 (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); 
在经过路由抉择后,所有需要本机转发的报文都会交由 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 所指向的函数。
0 0