Netfilter分析

1020阅读 0评论2015-12-02 qxhgd
分类:LINUX

一、概述

1. Netfilter/IPTables框架简介

          Netfilter/IPTables是继2.0.xIPfwadm2.2.xIPchains之后,新一代的Linux防火墙机制。Netfilter采用模块化设计,具有良好的可扩充性。其重要工具模块IPTables连接到Netfilter的架构中,并允许使用者对数据报进行过滤、地址转换、处理等操作。

         Netfilter提供了一个框架,将对网络代码的直接干涉降到最低,并允许用规定的接口将其他包处理代码以模块的形式添加到内核中,具有极强的灵活性。

2. 主要源代码文件

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

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

         头文件:include/linux/netfilter_ipv4.h

                                      include/linux/netfilter_ipv4/*.h

                        ip_input.cip_forward.cip_output.cip_fragment.c


二、Netfilter/IPTables-IPv4总体架构

        Netfilter主要通过表、链实现规则,可以这么说,Netfilter是表的容器,表是链的容器,链是规则的容器,最终形成对数据报处理规则的实现。

   详细地说,Netfilter/IPTables的体系结构可以分为三个大部分:

1. NetfilterHOOK机制

        Netfilter的通用框架不依赖于具体的协议,而是为每种网络协议定义一套HOOK函数。这些HOOK函数在数据报经过协议栈的几个关键点时被调用,在这几个点中,协议栈将数据报及HOOK函数标号作为参数,传递给Netfilter框架。

         对于它在网络堆栈中增加的这些HOOK,内核的任何模块可以对每种协议的一个或多个HOOK进行注册,实现挂接。这样当某个数据报被传递给Netfilter框架时,内核能检测到是否有任何模块对该协议和HOOK函数进行了注册。若注册了,则调用该模块的注册时使用的回调函数,这样这些模块就有机会检查、修改、丢弃该数据报及指示Netfilter将该数据报传入用户空间的队列。

    这样,HOOK提供了一种方便的机制:在数据报通过Linux内核的不同位置上截获和操作处理数据报。

2. IPTables基础模块

        IPTables基础模块实现了三个表来筛选各种数据报,具体地讲,Linux2.4内核提供的这三种数据报的处理功能是相互间独立的模块,都基于NetfilterHOOK函数和各种表、链实现。这三个表包括:filter表,nat表以及mangle表。

3. 具体功能模块

    1. 数据报过滤模块

    2. 连接跟踪模块(Conntrack

    3. 网络地址转换模块(NAT

    4. 数据报修改模块(mangle

    5. 其它高级功能模块

于是,Netfilter/IPTables总体架构如图http://blog.chinaunix.net/photo/24896_061206192251.jpg所示

三、HOOK的实现

1. Netfilter-IPv4中的HOOK

        Netfilter模块需要使用HOOK来启用函数的动态钩接,它在IPv4中定义了五个HOOK(位于文件include/linux/netfilter_ipv4.hLine 39),分别对应0-4hooknum

简单地说,数据报经过各个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处理后发送出去。

    总之,这五个HOOK所组成的Netfilter-IPv4数据报筛选体系如图http://blog.chinaunix.net/photo/24896_061206192311.jpg: (注:下面所说Netfilter/IPTables均基于IPv4,不再赘述)

详细地说,各个HOOK及其在IP数据报传递中的具体位置如图http://blog.chinaunix.net/photo/24896_061206192340.jpg

    数据报在进入路由代码被处理之前,数据报在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)

    目的地为本地主机的数据报在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)

    目的地非本地主机的数据报,包括被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)

    本地主机发出的数据报在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)

    所有数据报,包括源地址为本地主机和非本地主机的,在通过网络设备离开本地主机之前,在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)

    其中,入口为net_rx_action()(位于net/core/dev.cLine1602),作用是将数据报一个个地从CPU的输入队列中拿出,然后传递给协议处理例程。

    出口为dev_queue_xmit()(位于net/core/dev.cLine1035),这个函数被高层协议的实例使用,以数据结构struct sk_buff *skb的形式在网络设备上发送数据报。

2. HOOK的调用

        HOOK的调用是通过宏NF_HOOK实现的,其定义位于include/linux/Netfilter.hLine122

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

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

? (okfn)(skb) \

: nf_hook_slow((pf), (hook), (skb), (indev), (outdev), (okfn)))

    这里先调用list_empty函数检查HOOK点存储数组nf_hooks是否为空,为空则表示没有HOOK注册,则直接调用okfn继续处理。如果不为空,则转入nf_hook_slow()函数。

        nf_hook_slow()函数(位于net/core/netfilter.cLine449)的工作主要是读nf_hook数组遍历所有的nf_hook_ops结构,并调用nf_hookfn()处理各个数据报。

    即HOOK的调用过程如图http://blog.chinaunix.net/photo/24896_061206192356.jpg所示

下面说明一下NF_HOOK的各个参数:

网卡收到IP分组报文后,将它们放入sk_buff,然后再传送给网络堆栈,网络堆栈几乎一直要用到sk_buff。其定义在include/linux/skbuff.hLine 129,下面列出我认为对分析有意义的部分成员:

#define PACKET_HOST 0 // 发送到本机的报文

#define PACKET_BROADCAST 1 // 广播报文

#define PACKET_MULTICAST 2 // 多播报文

#define PACKET_OTHERHOST 3 // 表示目的地非本机但被本机 接收的报文

#define PACKET_OUTGOING 4 // 离开本机的报文

/* These ones are invisible by user level */

#define PACKET_LOOPBACK 5 // 本机发给自己的报文

#define PACKET_FASTROUTE 6 // 快速路由报文

3. HOOK点的实现

    对应于各个不同协议的不同HOOK点是由一个二维数组nf_hooks存储的(位于net/core/netfilter.cLine 47),具体的HOOK点则由数据结构nf_hook_ops(位于include/linux/netfilter.hLine 44)实现。如图http://blog.chinaunix.net/photo/24896_061206192528.jpg所示:

其中,nf_hook_ops成员中:

enum NF_IP_hook_priorities {

NF_IP_PRI_FIRST = INT_MIN,

NF_IP_PRI_CONNTRACK= -200,

NF_IP_PRI_MANGLE = -150,

NF_IP_PRI_NAT_DST = -100,

NF_IP_PRI_FILTER = 0,

NF_IP_PRI_NAT_SRC = 100,

NF_IP_PRI_LAST = INT_MAX,

};

typedef unsigned int nf_hookfn(unsigned int hooknum,

struct sk_buff **skb,

const struct net_device *in,

const struct net_device *out,

int (*okfn)(struct sk_buff *));

    这是nf_hook_ops中最关键的成员,其五个参数分别对应前面所解释的NF_HOOK中弟26个参数

    调用HOOK的包筛选函数必须返回特定的值,这些值以宏的形式定义于头文件include/linux/netfilter.h中(Line15),分别为:

4. HOOK的注册和注销

        HOOK的注册和注销分别是通过nf_register_hook()函数和nf_unregister_hook()函数(分别位于net/core/netfilter.cLine6076)实现的,其参数均为一个nf_hook_ops结构,二者的实现也非常简单。

        nf_register_hook()的工作是首先遍历nf_hools[][],由HOOK的优先级确定在HOOK链表中的位置,然后根据优先级将该HOOKnf_hook_ops加入链表;

        nf_unregister_hook()的工作更加简单,其实就是将该HOOKnf_hook_ops从链表中删除。

四、IPTables系统

1. 表-规则系统

        IPTables是基于Netfilter基本架构实现的一个可扩展的数据报高级管理系统,利用tablechainrule三级来存储数据报的各种规则。系统预定义了三个table

监听NF_IP_LOCAL_INNF_IP_FORWARDNF_IP_LOCAL_OUT三个HOOK,作用是在所有数据报传递的关键点上对其进行过滤。

监听NF_IP_PRE_ROUTINGNF_IP_POST_ROUTINGNF_IP_LOCAL_OUT三个HOOK,作用是当新连接的第一个数据报经过时,在nat表中决定对其的转换操作;而后面的其它数据报都将根据第一个数据报的结果进行相同的转换处理。

监听NF_IP_PRE_ROUTINGNF_IP_LOCAL_OUT两个HOOK,作用是修改数据报报头中的一些值。

2. 表的实现

struct ipt_table

{

struct list_head list; // 一个双向链表

char name[IPT_TABLE_MAXNAMELEN]; // 被用户空间使用的表函数的名字

struct ipt_replace *table; // 表初始化的模板,定义了一个初始化用的该 // 表的所默认的HOOK所包含的规则等信息,

// 用户通过系统调用进行表的替换时也要用

unsigned int valid_hooks; // 表所监听的HOOK,实质是一个位图

rwlock_t lock; // 整个表的读/写自旋锁

struct ipt_table_info *private; // 表所存储的数据信息,也就是实际的数据区,

// 仅在处理ipt_table的代码内部使用

struct module *me; // 如果是模块,那么取THIS_MODULE,否则取NULL

};

其中:

struct ipt_replace

{

char name[IPT_TABLE_MAXNAMELEN];

unsigned int valid_hooks;

unsigned int num_entries; // 规则表入口的数量

unsigned int size; // 新的规则表的总大小

/* Hook entry points. */

unsigned int hook_entry[NF_IP_NUMHOOKS]; // 表所监听HOOK的规则入口,

// 是对于entries[ ]的偏移

unsigned int underflow[NF_IP_NUMHOOKS]; // 规则表的最大下界

unsigned int num_counters; // 旧的计数器数目,即当前的旧entries的数目

struct ipt_counters *counters; // 旧的计数器

struct ipt_entry entries[0]; // 规则表入口

};

struct ipt_table_info

{

unsigned int size;

unsigned int number; // 表项的数目

unsigned int initial_entries; // 初始表项数目

unsigned int hook_entry[NF_IP_NUMHOOKS]; // 所监听HOOK的规则入口

unsigned int underflow[NF_IP_NUMHOOKS]; // 规则表的最大下界

char entries[0] ____cacheline_aligned; // 规则表入口,即真正的规则存储结构 // ipt_entry组成块的起始地址,对多CPU,每个CPU对应一个

};

3. 规则的实现

        IPTables中的规则表可以在用户空间中使用,但它所采用的数据结构与内核空间中的是一样的,只不过有些成员不会在用户空间中使用。

    一个完整的规则由三个数据结构共同实现,分别是:

    下面将依次对这三个数据结构进行分析:

  1. 存储规则整体的结构ipt_entry,其形式是一个链表(位于include/linux/netfilter_ipv4/ip_tables.hLine122):

struct ipt_entry

{

struct ipt_ip ip;

unsigned int nfcache;

u_int16_t target_offset;

u_int16_t next_offset;

unsigned int comefrom;

struct ipt_counters counters;

unsigned char elems[0];

};

    其成员如下:

#define NFC_ALTERED 0x8000 //已改变

#define NFC_UNKNOWN 0x4000 //不确定

另一个可能值是0,即没有改变。

  1. 扩展match的存储结构ipt_entry_match,位于include/linux/netfilter_ipv4/ip_tables.hLine48

struct ipt_entry_match

{

union {

struct {

u_int16_t match_size;

char name[IPT_FUNCTION_MAXNAMELEN];

} user;

struct {

u_int16_t match_size;

struct ipt_match *match;

} kernel;

u_int16_t match_size; //总长度

} u;

unsigned char data[0];

};

    其中描述match大小的`u_int16_t match_size;`,从涉及这个变量的源码看来,在使用的时候需要注意使用一个宏IPT_ALIGN(位于include/linux/netfilter_ipv4/ip_tables.hLine445)来进行4的对齐处理(0x3 & 0xfffffffc),这应该是由于matchtarget扩展后大小的不确定性决定的。

    在结构中,用户空间与内核空间为不同的实现,内核空间中的描述拥有更多的信息。在用户空间中存放的仅仅是match的名称,而在内核空间中存放的则是一个指向ipt_match结构的指针

结构ipt_match位于include/linux/netfilter_ipv4/ip_tables.hLine342

struct ipt_match

{

struct list_head list;

const char name[IPT_FUNCTION_MAXNAMELEN];

int (*match)(const struct sk_buff *skb,

const struct net_device *in,

const struct net_device *out,

const void *matchinfo, // 指向规则中match数据的指针,

// 具体是什么数据结构依情况而定

int offset, // IP数据报的偏移

const void *hdr, // 指向协议头的指针

u_int16_t datalen, // 实际数据长度,即数据报长度-IP头长度

int *hotdrop);

int (*checkentry)(const char *tablename, // 可用的表

const struct ipt_ip *ip,

void *matchinfo,

unsigned int matchinfosize,

unsigned int hook_mask); // 对应HOOK的位图

void (*destroy)(void *matchinfo, unsigned int matchinfosize);

struct module *me;

};

其中几个重要成员:

  1. 扩展target的存储结构ipt_entry_target,位于include/linux/netfilter_ipv4/ip_tables.hLine71,这个结构与ipt_entry_match结构类似,同时其中描述内核空间target的结构ipt_target(位于include/linux/netfilter_ipv4/ip_tables.hLine375)也与ipt_match类似,只不过其中的target()函数返回值不是0/1,而是verdict

    而target的实际使用中,是用一个结构ipt_standard_target专门来描述,这才是实际的target描述数据结构(位于include/linux/netfilter_ipv4/ip_tables.hLine94),它实际上就是一个ipt_entry_target加一个verdict

if (v !=IPT_RETURN) {

verdict = (unsigned)(-v) - 1;

break;

}

其中的IPT_RETURN定义为:

#define IPT_RETURN (-NF_MAX_VERDICT – 1)

而宏NF_MAX_VERDICT实际上就是:

#define NF_MAX_VERDICT NF_REPEAT

    这样,实际上IPT_RETURN的值就是-NF_REPEAT-1,也就是对应REPEAT,这就是对执行动作的实际描述;而我们可以看到,在下面对verdict进行赋值时,它所使用的值是`(unsigned)(-v) – 1`,这就是在内核中实际对偏移进行定位时所使用的值。

        那么总之呢,表和规则的实现如图http://blog.chinaunix.net/photo/24896_061206192551.jpg所示:

    从上图中不难发现,match的定位如下:

        target的定位则为:

      这些对于理解match以及target相关函数的实现是很有必要明确的。


    同时,include/linux/netfilter_ipv4/ip_tables.h中提供了三个“helper functions”,可用于使对于entrytartgetmatch的操作变得方便,分别是:

4. 规则的使用

    当一个特定的HOOK被激活时,数据报就开始进入Netfilter/IPTables系统进行遍历,首先检查`struct ipt_ip ip`,然后数据报将依次遍历各个match,也就是`struct ipt_entry_match`,并执行相应的match函数,即ipt_match结构中的*match所指向的函数。当match函数匹配不成功时返回0,或者hotdrop被置为1时,遍历将会停止。

    对match的遍历完成后,会开始检查`struct ipt_entry_target`,其中如果是一个标准的target,那么会检查`struct ipt_standard_target`中的verdict,如果verdict值是正的而偏移却指向不正确的位置,那么ipt_entry中的comefrom成员就有了用武之地——数据报返回所经历的上一个规则。对于非标准的target呢,就会调用target()函数,然后根据其返回值进行后面的处理。

5. 规则的扩展

        Netfilter/IPTables提供了对规则进行扩展的机制:可以写一个LKM来扩展内核空间的功能,也可以写一个共享库来扩展用户空间中IPTables的功能。

  1. 内核的扩展

    要对内核空间的功能进行扩展,实际上就是写一个具有表、match以及target增加功能的模块,相关的函数为(位于net/ipv4/netfilter/ip_tables.cLine1318 to 1444):

        1. 对新表进行边界检查(由宏IPT_ENTRY_ITERATE()调用函数check_entry_size_and_blocks(),位于net/ipv4/netfilter/ip_tables.cLine732),包括对齐、过大过小等,特别是保证赋给hook_entriesunderflows值的正确性。

        2. 调用函数make_source_chains()(位于net/ipv4/netfilter/ip_tables.cLine499)检查新的表中是否存在规则环,同时将HOOK的规则遍历顺序存入comefrom变量。(这个函数我没有仔细看,只是大概略了一下)

        3. ipt_entry依次进行ipt_ip头、match以及target的完整性检查(由宏IPT_ENTRY_ITERATE()调用函数check_entry(),位于net/ipv4/netfilter/ip_tables.cLine676),保证ipt_entry的正确性。

        4. 将正确的 ipt_tables复制给其他的CPU

这个函数另外还在do_replace()函数(仅在一个源码中没有被调用过的函数中被调用,不予分析)中被调用。

    这三对函数除了ipt_register_table()外的5个函数主要就是互斥地将table/match/target加入到双向链表中或者从双向链表中删除。

    其中向双向链表中加入新节点是通过调用list_named_insert()函数(位于include/linux/netfilter_ipv4/listhelp.hLine101)实现的。这个函数的主要工作是首先确定待插入的match名字是否已经存在,只有不存在时才进行插入的操作。

  1. 用户空间的扩展

    用户空间中的扩展用的是共享库配合libiptc库的机制,但这种机制是在单独的IPTbales程序中提供的,内核源码中并没有提供,这里就不做分析了。

五、数据报过滤模块——filter

1. 概述

        filter表的功能仅仅是对数据报进行过滤,并不对数据报进行任何的修改。

        filter模块在Netfilter中是基于下列HOOK点的:

    这几个HOOK分别对应着filter表中的INPUTFORWARDOUTPUT三条规则链,对于任何一个数据报都会经过这3HOOK之一。

        filter模块的接口位于文件net/ipv4/netfilter/iptables_filter.c中。

2. filter表的定义和初始化

        filter表是前面所述数据结构ipt_table的一个实例,它的定义和初始化位于net/ipv4/netfilter/iptable_filter.cLine84

static struct ipt_table packet_filter

= { { NULL, NULL }, "filter", &initial_table.repl,

FILTER_VALID_HOOKS, RW_LOCK_UNLOCKED, NULL, THIS_MODULE };

    对照结构ipt_table的定义,我们可以发现,filter表的初始化数据为:

static struct

{

struct ipt_replace repl;

struct ipt_standard entries[3];

struct ipt_error term;

} initial_table __initdata

= { { "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, { } },

{

/* LOCAL_IN */

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

0,

sizeof(struct ipt_entry),

sizeof(struct ipt_standard),

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

{ { { { ipt_ALIGN(sizeof(struct ipt_standard_target)), "" } }, { } },

-NF_ACCEPT - 1 } },

/* FORWARD */

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

0,

sizeof(struct ipt_entry),

sizeof(struct ipt_standard),

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

{ { { { ipt_ALIGN(sizeof(struct ipt_standard_target)), "" } }, { } },

-NF_ACCEPT - 1 } },

/* LOCAL_OUT */

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

0,

sizeof(struct ipt_entry),

sizeof(struct ipt_standard),

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

{ { { { ipt_ALIGN(sizeof(struct ipt_standard_target)), "" } }, { } },

-NF_ACCEPT - 1 } }

},

/* ERROR */

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

0,

sizeof(struct ipt_entry),

sizeof(struct ipt_error),

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

{ { { { ipt_ALIGN(sizeof(struct ipt_error_target)), ipt_ERROR_TARGET } },

{ } },

"ERROR"

}

}

};

    我们可以看到,一个initial_table包含三个成员:

#define FILTER_VALID_HOOKS ((1 << NF_IP_LOCAL_IN) | (1 << NF_IP_FORWARD) | (1 << NF_IP_LOCAL_OUT))

我们可以看到,实际上就是INFORWARDOUT

3. filter表的实现

        filter表的实现函数实际上就是模块iptable_filter.oinit函数,位于net/ipv4/netfilter/iptable_filter.cLine128。其主要工作是首先通过ipt_register_table()函数进行表的注册,然后用nf_register_hook()函数注册表所监听的各个HOOK

    其中,对HOOK进行注册时,是通过对数据结构nf_hook_ops的一个实例ipt_ops进行操作来实现的,这个实例的定义及初始化位于net/ipv4/netfilter/iptable_filter.cLine117

static struct nf_hook_ops ipt_ops[]

= { { { NULL, NULL }, ipt_hook, PF_INET, NF_IP_LOCAL_IN, NF_IP_PRI_FILTER },

{ { NULL, NULL }, ipt_hook, PF_INET, NF_IP_FORWARD, NF_IP_PRI_FILTER },

{ { NULL, NULL }, ipt_local_out_hook, PF_INET, NF_IP_LOCAL_OUT,

NF_IP_PRI_FILTER }

};

    对应前面所分析nf_hook_ops的各个成员,不难确定这些初始化值的意义。

    其中,对应INFORWARD的处理函数均为ipt_hookOUT的处理函数则为ipt_local_out_hook,下面依次分析之:

static unsigned int

ipt_hook(unsigned int hook,

struct sk_buff **pskb,

const struct net_device *in,

const struct net_device *out,

int (*okfn)(struct sk_buff *))

{

return ipt_do_table(pskb, hook, in, out, &packet_filter, NULL);

}

    实际上它就是调用了ipt_do_table()函数,也就是说,注册时首先注册一个ipt_hook()函数,然后ipt_hook()通过调用ipt_do_table()函数对传入的数据进行真正的处理。下面我们来看一下ipt_do_table()这个函数:

    它位于net/ipv4/netfilter/ip_tables.cLine254,是一个很长的函数,其主要功能是对数据报进行各种匹配、过滤(包括基本规则、matches以及target),具体些说,其工作大致为:

  1. 初始化各种变量,如IP头、数据区、输入输出设备、段偏移、规则入口及偏移量等等;

  2. 进行规则的匹配,首先调用ip_packet_match()函数(位于net/ipv4/netfilter/ip_tables.cLine121)确定IP数据报是否匹配规则,若不匹配则跳到下一条规则(这个函数的主要工作大致为:依次处理源/目的IP地址、输入输出接口,然后对基本的规则进行匹配);

  3. 如果数据报匹配,则下面开始继续匹配matchestarget,首先利用宏IPT_MATCH_ITERATE调用do_match()函数(下面单独分析)对扩展的match进行匹配,若不匹配则跳到下一条规则;

  4. 扩展match匹配后,首先调用ipt_get_target()获得target的偏移地址,然后对target进行匹配,这个匹配的过程要比match的匹配过程复杂一些,同样在下面单独分析。

    下面首先来分析do_match()函数,它位于net/ipv4/netfilter/ip_tables.cLine229,它的实现只有一个if语句:

if (!m->u.kernel.match->match(skb, in, out, m->data, offset, hdr, datalen, hotdrop))

return 1;

else

return 0;

    其中的`m->u.kernel.match->match(skb, in, out, m->data, offset, hdr, datalen, hotdrop)`是用来定位match的。

    因为如果仅仅是根据match的名字遍历链表来进行查找的话,效率会非常低下。Netfilter源码中采用的方法是在进行match的检测之前,也就是在ipt_register_table()函数中通过translate_table()函数由宏IPT_ENTRY_ITERATE调用函数check_entry()时,在check_entry()中通过宏IPT_MATCH_ITERATE调用了check_match()函数(位于net/ipv4/netfilter/ip_tables.cLine640),在这个函数中,有一个对m->u.kernel.match的赋值:

m->u.kernel.match = match;

    这样,每条规则的u.kernel.match 就与内核模块中的struct ipt_match链表关联了起来,也就是说,这样一来,根据match的名字,其对应的match函数就与链表中对应的函数关联了起来。于是,上面的那条定位match的语句的意义也就开始明了了:

    利用宏IPT_MATCH_ITERATE来遍历规则中的所有mach,然后直接调用`m->u.kernel.match->match`来进行对数据报的匹配工作——这样的效率显然要比简单的遍历要高许多。

    然后我们来看一下对target的匹配,从数据结构的实现上看,似乎这个过程与match的匹配应该是相似的,但实际上target存在标准的和非标准的两种,其中标准的target与非标准的target的处理是不一样的。在这里我遇到了问题,如下:

    首先,在Netfilter的源码中,存在两个ipt_standard_target,其中一个是一个struct,位于include/linux/netfilter_ipv4/ip_tables.hLine94;另一个是`struct ipt_target`的一个实例,位于net/ipv4/netfilter/IPtables.cLine1684,而在target的匹配过程中,它是这样处理的(ipt_do_tables()net/ipv4/netfilter/ip_tables.cLine329):

/* Standard target? */

if (!t->u.kernel.target->target) {……}

    从这里看来,它应该是当 t->u.kernel.targettarget函数为空时,表明其为标准的target。那么结合上述两者的定义,似乎用的是后者,因为后者的定义及初始化如下:

/* The built-in targets: standard (NULL) and error. */

static struct ipt_target ipt_standard_target

= { { NULL, NULL }, IPT_STANDARD_TARGET, NULL, NULL, NULL };

    但是问题出现在:初始化中的IPT_STANDARD_TARGET被定义为””!!并且在整个源码中,用到实例化的ipt_standard_target的地方仅有两处,即上面的这个定义以及ip_tables.c中将ipt_standard_target加入到target链表之中。也就是说这个实例的名字一直为空,这一点如何理解?

    这样,到这里,filter表的实现已经分析完毕,至于具体的过滤功能如何实现,那就是每个HOOK处理函数的问题了。

六、连接跟踪模块(Conntrack

1. 概述

    连接跟踪模块是NAT的基础,但作为一个单独的模块实现。它用于对包过滤功能的一个扩展,管理单个连接(特别是TCP连接),并负责为现有的连接分配输入、输出和转发IP数据报,从而基于连接跟踪来实现一个“基于状态”的防火墙。

    当连接跟踪模块注册一个连接建立包之后,将生成一个新的连接记录。此后,所有属于此连接的数据报都被唯一地分配给这个连接。如果一段时间内没有流量而超时,连接将被删除,然后其他需要使用连接跟踪的模块就可以重新使用这个连接所释放的资源了。

    如果需要用于比传输协议更上层的应用协议,连接跟踪模块还必须能够将建立的数据连接与现有的控制连接相关联。

        ConntrackNetfilter中是基于下列HOOK的:

    在所有的HOOK上,NF_IP_PRI_CONNTRACK的优先级是最高的(-200),这意味着每个数据报在进入和发出之前都首先要经过Conntrack模块,然后才会被传到钩在HOOK上的其它模块。

        Conntrack模块的接口位于文件net/ipv4/netfilter/ip_conntrack_standalone.c中。

2. 连接状态的管理

  1. 多元组

    在连接跟踪模块中,使用所谓的“tuple”,也就是多元组,来小巧锐利地描述连接记录的关键部分,主要是方便连接记录的管理。其对应的数据结构是ip_conntrack_tuple(位于include/linux/netfilter_ipv4/ip_conntrack_tuple.hLine38):

struct ip_conntrack_tuple

{

struct ip_conntrack_manip src;

struct {

u_int32_t ip;

union {

u_int16_t all;

struct { u_int16_t port; } tcp;

struct { u_int16_t port; } udp;

struct { u_int8_t type, code; } icmp;

} u;

u_int16_t protonum;

} dst;

};

    从它的定义可以看出,一个多元组实际上包括两个部分:一是所谓的“unfixed”部分,也就是源地址以及端口;二是所谓的“fixed”部分,也就是目的地址、端口以及所用的协议。这样,连接的两端分别用地址+端口,再加上所使用的协议,一个tuple就可以唯一地标识一个连接了(对于没有端口的icmp协议,则用其它东东标识)。

  1. 连接记录

    那么真正的完整连接记录则是由数据结构ip_conntrack(位于include/linux/netfilter_ipv4/ip_conntrack.hLine160)来描述的,其成员有:

  1. hash

        Netfilter使用一个hash表来对连接记录进行管理,这个hash表的初始指针为*ip_conntrack_hash,位于net/ipv4/netfilter/ip_conntrack_core.cLine65,这样我们就可以使用ip_conntrack_hash[num]的方式来直接定位某个连接记录了。

    而hash表的每个表项则由数据结构ip_conntrack_tuple_hash(位于include/linux/netfilter_ipv4/ip_conntrack_tuple.hLine86)描述

struct ip_conntrack_tuple_hash

{

struct list_head list;

struct ip_conntrack_tuple tuple;

struct ip_conntrack *ctrack;

};

    可见,一个hash表项中实质性的内容就是一个多元组ip_conntrack_tuple;同时还有一个指向连接的ip_conntrack结构的指针;以及一个链表头(这个链表头不知是干嘛的)。

3. 连接跟踪的实现

    有了以上的数据结构,连接跟踪的具体实现其实就非常简单而常规了,无非就是初始化连接记录的创建在连接跟踪hash表中搜索并定位数据报将数据报转换为一个多元组判断连接的状态以及方向超时处理协议的添加、查找和注销对不同协议的不同处理、以及在两个连接跟踪相关的HOOK上对数据报的处理

    下面重点说明一下我在分析中遇到的几个比较重要或者比较难理解的地方:

        可以将预期连接看作父子关系来理解,如图  http://blog.chinaunix.net/photo/24896_061206192612.jpg
        ip_conntrack的状态转换分两种,同样用图来描述。首先是正常的状态转换,如图http://blog.chinaunix.net/photo/24896_061206192631.jpg,
        然后是
ICMP error时的状态转换(由函数icmp_error_track()来判断,位于net/ipv4/netfilter/ip_conntrack_core.cLine495),
        如图
http://blog.chinaunix.net/photo/24896_061206192648.jpg
    在经过HOOK中的NF_IP_PRE_ROUTING时(函数ip_conntrack_in(),位于net/ipv4/netfilter/ip_conntrack_core.cLine796),由于外来的数据报有可能是经过分片的,所以必须对分片的情形进行处理,将IP数据报组装后才能分配给连接。
    具体的操作是首先由函数ip_ct_gather_frags()对分片的数据报进行收集,然后调用ip_defrag()函数(位于net/ipv4/ip_fragment.cLine632)组装之

4. 协议的扩展

    由于我们可能要添加新的协议,所以单独对协议的扩展进行分析。

    各种协议使用一个全局的协议列表存放,即protocol_list(位于include/linux/netfilter_ipv4/ip_conntrack_core.hLine21),使用结构ip_conntrack_protocol(位于include/linux/netfilter_ipv4/ip_conntrack_protocol.hLine6)来表示:

struct ip_conntrack_protocol

{

struct list_head list;

u_int8_t proto; //协议号

const char *name;

int (*pkt_to_tuple)(const void *datah, size_t datalen,

struct ip_conntrack_tuple *tuple);

int (*invert_tuple)(struct ip_conntrack_tuple *inverse,

const struct ip_conntrack_tuple *orig);

unsigned int (*print_tuple)(char *buffer,

const struct ip_conntrack_tuple *);

unsigned int (*print_conntrack)(char *buffer,

const struct ip_conntrack *);

int (*packet)(struct ip_conntrack *conntrack,

struct iphdr *iph, size_t len,

enum ip_conntrack_info ctinfo);

int (*new)(struct ip_conntrack *conntrack, struct iphdr *iph,

size_t len);

void (*destroy)(struct ip_conntrack *conntrack);

int (*exp_matches_pkt)(struct ip_conntrack_expect *exp,

struct sk_buff **pskb);

struct module *me;

};

    其中重要成员:

    添加/删除协议使用的是函数ip_conntrack_protocol_register()以及ip_conntrack_protocol_unregister(),分别位于net/ipv4/netfilter/ip_conntrack_standalone.cLine298 & 320,其工作就是将ip_conntrack_protocol添加到全局协议列表protocol_list

七、网络地址转换模块(Network Address Translation

1. 概述

    网络地址转换的机制一般用于处理IP地址转换,在Netfilter中,可以支持多种NAT类型,而其实现的基础是连接跟踪。

        NAT可以分为SNATDNAT,即源NAT和目的NAT,在Netfilter中分别基于以下HOOK

    同时,MASQUERADE(伪装)是SNAT的一种特例,它与SNAT几乎一样,只有一点不同:如果连接断开,所有的连接跟踪信息将被丢弃,而去使用重新连接以后的IP地址进行IP伪装;而REDIRECT(重定向)是DNAT的一种特例,这时候就相当于将符合条件的数据报的目的IP地址改为数据报进入系统时的网络接口的IP地址。

2. 基于连接跟踪的相关数据结构

    NAT是基于连接跟踪实现的,NAT中所有的连接都由连接跟踪模块来管理,NAT模块的主要任务是维护nat表和进行实际的地址转换。这样,我们来回头重新审视一下连接跟踪模块中由条件编译决定的部分。

    首先,是连接的描述ip_conntrack,在连接跟踪模块部分中提到,这个结构的最后有“NAT模块设置的信息”,即:

#ifdef CONFIG_IP_NF_NAT_NEEDED

struct {

struct ip_nat_info info;

union ip_conntrack_nat_help help;

#if defined(CONFIG_IP_NF_TARGET_MASQUERADE) || \

defined(CONFIG_IP_NF_TARGET_MASQUERADE_MODULE)

int masq_index;

#endif

} nat;

#endif /* CONFIG_IP_NF_NAT_NEEDED */

这是一个叫做nat的子结构,其中有3个成员:

    好,下面我们来看一下这个ip_nat_info结构,这个结构存储了连接中的地址绑定信息,其定义位于include/linux/netfilter_ipv4/ip_nat.hLine98

struct ip_nat_info

{

int initialized;

unsigned int num_manips;

struct ip_nat_info_manip manips[IP_NAT_MAX_MANIPS];

const struct ip_nat_mapping_type *mtype;

struct ip_nat_hash bysource, byipsproto;

struct ip_nat_helper *helper;

struct ip_nat_seq seq[IP_CT_DIR_MAX];

};


3. nat表的实现

        nat表的初始化和实现与filter极为相似。

    在初始化上,其初始化位于net/ipv4/netfilter/ip_nat_rule.cLine104,初始化所用模板则位于net/ipv4/netfilter/ip_nat_rule.cLine50

    在实现上,其实现函数就是NAT模块的初始化函数init_or_cleanup()(位于net/ipv4/netfilter/ip_nat_standalone.cLine278)。其工作主要是依次调用ip_nat_rule_init()ip_nat_init()以及nf_register_hook()

  1. 首先ip_nat_rule_init()(位于net/ipv4/netfilter/ip_nat_rule.cLine278)调用ipt_register_table()来初始化并注册nat表,然后利用ipt_register_target()来初始化并注册SNATDNAT,在这个注册过程中,关键的函数是ip_nat_setup_info(位于net/ipv4/netfilter/ip_nat_core.cLine511),其工作是:

  1. 然后ip_nat_init()(位于net/ipv4/netfilter/ip_nat_core.cLine953)会给nat所用的两个hash表(bysourcebyipsproto)分配空间并初始化各种协议

  2. 最后会通过nf_register_hook()注册相应HOOK的函数ip_nat_fn()ip_nat_local_fn()ip_nat_out(),并增加连接跟踪的计数器。

    在具体的HOOK函数实现上,后两者其实都是基于ip_nat_fn()的,而这其中最重要的处理函数,也就是实际的网络地址转换函数是do_bindings(),下面将对其进行分析:

        do_bindings()位于net/ipv4/netfilter/ip_nat_core.cLine747,其主要工作是将ip_nat_info中的地址绑定应用于数据报:

  1. 它首先在ip_nat_info->manip数组中查找能够匹配的绑定

  2. 然后调用manip_pkt()函数(位于net/ipv4/netfilter/ip_nat_core.cLine701)进行相应的地址转换:

  1. 最后调用helper模块进行执行(当然,如果有的话),特别是避免在同一个数据报上执行多次同一个helper模块


        nat表与filter表还有一个较大的不同:在一个连接中,只有第一个数据报才会经过nat表,而其转换的结果会作用于此连接中的其它所有数据报。

4. 协议的扩展

    要想扩展NAT的协议,那么必须写入两个模块,一个用于连接跟踪,一个用于NAT实现。

    与连接跟踪类似,nat中协议也由一个列表protos存放,位于include/linux/netfilter_ipv4/ip_nat_core.hLine17。协议的注册和注销分别是由函数ip_nat_protocol_register()ip_nat_protocol_unregister()实现的,位于net/ipv4/netfilter/ip_nat_standalone.cLine242

        nat的协议是由结构ip_nat_protocol描述的,其定义位于include/linux/netfilter_ipv4/ip_nat_protocol.hLine10

struct ip_nat_protocol

{

struct list_head list;

const char *name;

unsigned int protonum;

void (*manip_pkt)(struct iphdr *iph, size_t len,

const struct ip_conntrack_manip *manip,

enum ip_nat_manip_type maniptype);

int (*in_range)(const struct ip_conntrack_tuple *tuple,

enum ip_nat_manip_type maniptype,

const union ip_conntrack_manip_proto *min,

const union ip_conntrack_manip_proto *max);

int (*unique_tuple)(struct ip_conntrack_tuple *tuple,

const struct ip_nat_range *range,

enum ip_nat_manip_type maniptype,

const struct ip_conntrack *conntrack);

unsigned int (*print)(char *buffer,

const struct ip_conntrack_tuple *match,

const struct ip_conntrack_tuple *mask);

unsigned int (*print_range)(char *buffer,

const struct ip_nat_range *range);

};

    其中重要成员:

八、数据报修改模块──mangle

1. 概述

        mangle这个词的原意是撕裂、破坏,这里所谓“packet mangling”是指对packet的一些传输特性进行修改。mangle表被用来真正地对数据报进行修改,它可以在所有的5个HOOK上进行操作。

    从源码看来,mangle表中所允许修改的传输特性目前有:

2. mangle表的实现

    遍历整个源码,没有发现mangle表的独有数据结构。

        mangle表的初始化和实现与filter极为相似。在初始化上,其初始化位于net/ipv4/netfilter/iptable_mangle.cLine117,初始化所用模板则位于net/ipv4/netfilter/iptable_mangle.cLine43

    在实现上,其实现函数就是mangle模块的初始化函数init()(位于net/ipv4/netfilter/iptable_mangle.cLine183)。其工作就是依次注册packet_mangler表以及5HOOK。其中NF_IP_LOCAL_OUT的处理函数为ipt_local_hook(),其余均为ipt_route_hook(),分别位于net/ipv4/netfilter/iptable_mangle.cLine132 & 122,二者的关键实现都是通过调用ipt_do_table()实现的。

3. 数据报的修改

    对数据报不同位的修改都是通过单独的模块实现的,也就是说由ipt_tos.oipt_ttl.oipt_mark.oipt_tcpmss.o实现上面所说的四种传输特性的修改。

    以TOS为例,其涉及的文件为net/ipv4/netfilter/ipt_tos.c以及include/linux/netfilter_ipv4/ipt_tos.h

    其专有数据结构为ipt_tos_info,定义于ipt_tos.h中:

struct ipt_tos_info {

u_int8_t tos;

u_int8_t invert;

};

    其模块的添加/卸载函数很简单,其实就是添加/删除TOSMATCHtos_match(定义并初始化于ipt_tos.cLine37):

static struct ipt_match tos_match

= { { NULL, NULL }, "tos", &match, &checkentry, NULL, THIS_MODULE };

    而在tos_match的处理函数match()中,已经完成了对相应位的赋值,这似乎是违反match仅仅匹配而不修改的一个特例。

九、其它高级功能模块

        Netfilter中还有一些其它的高级功能模块,基本是为了用户操作方便的,没有对它们进行分析,如:

P.S. 字体好乱啊。。。要是CUBlog支持直接导入odt文档就好了。。。粘过来就大变样。。。-_-!
上一篇:Linux-kernel网桥代码分析(二)
下一篇:Netfilter分析(2)