1. 什么是Netfiler
Linux 从2.4.X 开始,引入了Netfilter,代替了原来的ipchain,什么是Netfilter呢?有人将它称为“Linux下一个优秀的防火墙工具”,这样讲,有一定的道理,但是却是很片面的。Netfilter 更准确地讲是Linux 内核中,一个包过滤框架,默认地,它在这个框架上实现了包过滤、状态检测、网络地址转换和包标记等多种功能,因为它设计的开放性,任何有内核开发经验的开发人员,也可以很容易地利用它提供接口,在内核的数据链路层、网络层,实现自己的功能模块。Netfilter的用户空间管理工具,是著名的iptables 工具套件。
Netfilter框架之所以能实现许多强大的功能,是因为它在内核若干网络转发的关键函数,设计了许多巧妙的钩子函数,比如数据转发,由两个主要函数A和B函数实现,流程为A->B ,现在改变为A->钩子函数->B,就这么简单,在本章里,就让我们来看看Netfilter框架的设计与实现。
2. 从NF_HOOK 谈起
在整个Netfilter中,NF_HOOK宏占有重要的作用,它定义在Netfilter.h中:
/* This is gross, but inline doesn't cut it for avoiding the function
call in fast path: gcc doesn't inline (needs value tracking?). --RR */
#ifdef CONFIG_NETFILTER_DEBUG
#define NF_HOOK(pf, hook, skb, indev, outdev, okfn) \
({int __ret; \
if ((__ret=nf_hook_slow(pf, hook, &(skb), indev, outdev, okfn, INT_MIN)) == 1) \
__ret = (okfn)(skb); \
__ret;})
#define NF_HOOK_THRESH(pf, hook, skb, indev, outdev, okfn, thresh) \
({int __ret; \
if ((__ret=nf_hook_slow(pf, hook, &(skb), indev, outdev, okfn, thresh)) == 1) \
__ret = (okfn)(skb); \
__ret;})
#else
#define NF_HOOK(pf, hook, skb, indev, outdev, okfn) \
({int __ret; \
if (list_empty(&nf_hooks[pf][hook]) || \
(__ret=nf_hook_slow(pf, hook, &(skb), indev, outdev, okfn, INT_MIN)) == 1) \
__ret = (okfn)(skb); \
__ret;})
#define NF_HOOK_THRESH(pf, hook, skb, indev, outdev, okfn, thresh) \
({int __ret; \
if (list_empty(&nf_hooks[pf][hook]) || \
(__ret=nf_hook_slow(pf, hook, &(skb), indev, outdev, okfn, thresh)) == 1) \
__ret = (okfn)(skb); \
__ret;})
#endif
一开始就摆出这么长一个宏出来,这个宏稍长了一点,缩短来看:
#ifdef CONFIG_NETFILTER_DEBUG
……
#else
……
#endif
我们暂且忽略用于调试作用的语句,把 #else中NF_HOOK的定义提取出来:
#define NF_HOOK(pf, hook, skb, indev, outdev, okfn) \
({int __ret; \
if (list_empty(&nf_hooks[pf][hook]) || \
(__ret=nf_hook_slow(pf, hook, &(skb), indev, outdev, okfn, INT_MIN)) == 1) \
__ret = (okfn)(skb); \
__ret;})
首先来看这个宏里边的二维数组nf_hooks[pf][hook] ,其中pf对应协议簇,hook对应了某个hook点,比如ipv4 协议簇(PF_INET)下有一个钩子NF_IP_PRE_ROUTING(路由查找之前),那么这个Hook点对应的这个二维数组中的元素就是nf_hooks[PF_INET][ NF_IP_PRE_ROUTING]。
nf_hooks数组是一个struct nf_hooks_ops 结构,这个结构有一个hook成员,指向这个hook点的hook函数。另一方面,同一个hook点,可能同时注册了多个hook,所以,结构struct nf_hooks_ops 中有一个list成员,用来维护一个hook点的链表。该结构定义如下:
struct nf_hook_ops
{
struct list_head list; //链表成员
/* User fills in from here down. */
nf_hookfn *hook; //链子函数指针
struct module *owner;
int pf; //协议簇,对于ipv4而言,是PF_INET
int hooknum; //hook类型
/* Hooks are ordered in ascending priority. */
int priority; //优先级
};
在一个结构中,内嵌一个struct list_head 类型的list成员,用来维护一个双向链表,是Linux 内核双向链表的标准用法。而list_empty函数用以判断这个链表是否为空。所以,整个NF_HOOK,其含义为:
如果当前pf协议的当前hook类型没有定义钩子函数,则直接执行okfn指向的函数,否则,就调用函数nf_hook_slow函数,当该函数返回值为1,仍继续调用okfn指向的函数。
举个例子来讲,ip_input.c中,ip_rcv 函数用于处理本机接受的数据包,处理完成后,按照正常顺序会调用 ip_rcv_finish函数,我们来看ip_rcv的最后一句:
return NF_HOOK(PF_INET, NF_IP_PRE_ROUTING, skb, dev, NULL,
ip_rcv_finish);
对照上面分析的NF_HOOK的实现,我们可以得出以下结论:
在内核收到一个发往本机的数据包,会判断PF_INET 协议的NF_IP_PRE_ROUTING HOOK类型下是否注册有钩子函数,如果没有,则直接继续执行ip_rcv_finish,如果有,则调用nf_hook_slow函数,进而进一步调用已注册的钩子函数,再根据其返回值,看是否还需要继续执行ip_rcv_finish。
这就是整个Linux内核Netfilter Hook的含义。
仔细阅读内核源码,可以发现,不仅在ip_input.c中,内核在每一个数据转发的关键节点,都放置了NF_HOOK,一个经典的IPV4的Netfilter Hook可以用下图来描述:
其中,绿色的部份即为Hook的类型,可以分为五类:
/* IP Hooks */
/* After promisc drops, checksum checks. */
#define NF_IP_PRE_ROUTING 0 //路由前,进入本机的数据
/* If the packet is destined for this box. */
#define NF_IP_LOCAL_IN 1 //路由后,进入本机的数据
/* If the packet is destined for another interface. */
#define NF_IP_FORWARD 2 //路由后,本机转发的数据
/* Packets coming from a local process. */
#define NF_IP_LOCAL_OUT 3 //路由前,本机本地进程发出的数据
/* Packets about to hit the wire. */
#define NF_IP_POST_ROUTING 4 //路由后,本机发出的数据
我们可以从上图中看出,我们常用的数据包过滤的表filter,在NF_IP_LOCAL_IN、NF_IP_FORWARD、NF_IP_LOCAL_OUT三处注册了钩子函数,以实现三种不同类型的包过滤,另外两个表也与此类似。
3. Filter表及钩子函数的注册
内核在网络堆栈的重要节点,引入了NF_HOOK宏,搭起了整个Netfilter的框架,但是NF_HOOK宏事实上仅是一个转向,更重要的内容是,“内核是如何注册钩子函数以及如何使用它们?”。内核默认的三个表,从框架的角度上来看,这些动作都是一致,我们以filter表为例,来回答这个问题。
要在内核中使用filter表,首先要向内核注册这个表,然后该表在NF_IP_LOCAL_IN、NF_IP_FORWARD、NF_IP_LOCAL_OUT三个Hook点,注册相应的钩子函数,在内核filter模块的初始化函数(iptable_filter.c),完成了这一功能:
static int __init 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;
/* 注册filter 表 */
ret = ipt_register_table(&packet_filter, &initial_table.repl);
if (ret < 0)
return ret;
/*依次注册filter表的三个钩子 */
ret = nf_register_hook(&ipt_ops[0]);
if (ret < 0)
goto cleanup_table;
ret = nf_register_hook(&ipt_ops[1]);
if (ret < 0)
goto cleanup_hook0;
ret = nf_register_hook(&ipt_ops[2]);
if (ret < 0)
goto cleanup_hook1;
return ret;
cleanup_hook1:
nf_unregister_hook(&ipt_ops[1]);
cleanup_hook0:
nf_unregister_hook(&ipt_ops[0]);
cleanup_table:
ipt_unregister_table(&packet_filter);
return ret;
}
表的注册
表的注册,是通过调用ipt_register_table 函数来实现的,我们先来看它的两个参数packet_filter和initial_table.repl。
packet_filter的定义和赋值:
static struct ipt_table packet_filter = {
.name = "filter",
.valid_hooks = FILTER_VALID_HOOKS,
.lock = RW_LOCK_UNLOCKED,
.me = THIS_MODULE
};
它是一个struct ipt_table类型,内核用结构struct ipt_table(ip_tables.h)来描述表:
struct ipt_table
{
/*链表成员*/
struct list_head list;
/* 表名,如"filter"、"nat"等,为了满足自动模块加载的设计,包含该表的模块应命名为iptable_'name'.o */
char name[IPT_TABLE_MAXNAMELEN];
/* 位向量,表示当前表影响了哪些(个)HOOK 类型*/
unsigned int valid_hooks;
/* 读写锁,初始为打开状态 */
rwlock_t lock;
/* iptable的数据区*/
struct ipt_table_info *private;
/* 是否在模块中定义,若否,则为NULL */
struct module *me;
};
先来看成员valid_hook,它 表示当前表影响了哪些hook 类型,我们前边谈到,filter 表影响了NF_IP_LOCAL_IN等三个Hook点,这里给它赋的值是FILTER_VALID_HOOKS,来看这个宏的实现:
#define FILTER_VALID_HOOKS ((1 << NF_IP_LOCAL_IN) | (1 << NF_IP_FORWARD) | (1 << NF_IP_LOCAL_OUT))
一个简单的按位或,对应了我们提到的filter表所影响的三个Hook点。
这个结构包含了表的名称,以及表所影响的Hook类型外,并没有其它实质性的东西,诸如我们关心的过滤规则等等,所以,private成员就成一个让人感兴趣的目标。
Private是一个struct ipt_table_info结构类型,该结构是实际描述表具体属性的数据结构(net/ipv4/netfilter/ip_tables.c):
/* The table itself */
struct ipt_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];
/* 每个CPU的Hook点规则表入口 */
char entries[0] ____cacheline_aligned;
};
ipt_register_table 函数调用中的第二个变量initial_table,它是表的初始化时使用的模板,其体体积相当的庞大,这里暂时只用到了其repl成员,它是一个struct ipt_replace结构,我们先来看这个成员赋值:
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, { } },
……
struct ipt_replace是我们要提到的第三个重要的数据结构,除了这里讲到的注册表时作为初始化模板,用户空间通过系统调用来操作表的时候也要用到这个结构类型的变量做为参数:
struct ipt_replace
{
/*前面的部份,类似于ipt_table_info*/
/* Which table. */
char name[IPT_TABLE_MAXNAMELEN];
/* Which hook entry points are valid: bitmask. You can't
change this. */
unsigned int valid_hooks;
/* Number of entries */
unsigned int num_entries;
/* Total size of new entries */
unsigned int size;
/* Hook entry points. */
unsigned int hook_entry[NF_IP_NUMHOOKS];
/* Underflow points. */
unsigned int underflow[NF_IP_NUMHOOKS];
/* 这个结构不同于ipt_table_info之处在于,它还要保存表的旧的规则信息: */
/* Number of counters (must be equal to current number of entries). */
unsigned int num_counters;
/* The old entries' counters. */
struct ipt_counters __user *counters;
/* The entries (hang off end: not really an array). */
struct ipt_entry entries[0];
};
结合上面的初始化赋值,我们可以对应过来:
name :"filter"
//filter表所影响的Hook类型
valid_hooks:FILTER_VALID_HOOKS
//初始化的规则数为4条,每个Hook类型(对应用户空间的“链”)初始化一条,并以一条“错误的规则”表示结束
num_entries:4
//标准的规则用struct ipt_standard,错误的规则用struct ipt_error描述
size:sizeof(struct ipt_standard) * 3 + sizeof(struct ipt_error)
//计算初始的各Hook点对应的初始规则的偏移值
hook_entry[NF_IP_NUMHOOKS]:
{ [NF_IP_LOCAL_IN] = 0,
[NF_IP_FORWARD] = sizeof(struct ipt_standard),
[NF_IP_LOCAL_OUT] = sizeof(struct ipt_standard) * 2 }
underflow[NF_IP_NUMHOOKS]:
{ [NF_IP_LOCAL_IN] = 0,
[NF_IP_FORWARD] = sizeof(struct ipt_standard),
[NF_IP_LOCAL_OUT] = sizeof(struct ipt_standard) * 2 }
//因为是初始化模块,不存在旧表,所以,这些保存旧表信息的参数均为空
num_counters:0
counters:NULL
entries[0]:{ }
OK,ipt_register_table函数的两个参数,前一个包含了表的基本信息,包含了表名,所影响的Hook类型,规则长度,入口等等,后一个是一个表的模块,即初始化的规则等等,有了这些基础,我们来看表的注册的实现:
int ipt_register_table(struct ipt_table *table, const struct ipt_replace *repl)
{
int ret;
struct ipt_table_info *newinfo;
static struct ipt_table_info bootstrap
= { 0, 0, 0, { 0 }, { 0 }, { } };
/*为每个CPU分配规则空间*/
newinfo = vmalloc(sizeof(struct ipt_table_info)
+ SMP_ALIGN(repl->size) * num_possible_cpus());
if (!newinfo)
return -ENOMEM;
/*将规则项拷贝到新表项的第一个cpu空间里面*/
memcpy(newinfo->entries, repl->entries, repl->size);
/*
* translate_table函数将newinfo表示的table的各个规则进行边界检查,然后对于
* newinfo所指的ipt_talbe_info结构中的hook_entries和underflows赋予正确的值,最
* 后将表项向其他cpu拷贝
*/
ret = translate_table(table->name, table->valid_hooks,
newinfo, repl->size,
repl->num_entries,
repl->hook_entry,
repl->underflow);
if (ret != 0) {
vfree(newinfo);
return ret;
}
ret = down_interruptible(&ipt_mutex);
if (ret != 0) {
vfree(newinfo);
return ret;
}
/* 如果注册的table已经存在,释放空间 并且递减模块计数 */
if (list_named_find(&ipt_tables, table->name)) {
ret = -EEXIST;
goto free_unlock;
}
/* 用新的table项替换旧的table项 */
table->private = &bootstrap;
if (!replace_table(table, 0, newinfo, &ret))
goto free_unlock;
duprintf("table->private->number = %u\n",
table->private->number);
/* 保存初始规则计数器 */
table->private->initial_entries = table->private->number;
rwlock_init(&table->lock);
/*将表添加进表的链表当中*/
list_prepend(&ipt_tables, table);
unlock:
up(&ipt_mutex);
return ret;
free_unlock:
vfree(newinfo);
goto unlock;
}
这里调用的一些重要的函数,我们后面会陆续分析到,实际上表的注册,就是一个建立/维护表的链表的过程,是最终通过调用内核链表函数list_prepend 来实现的,经过这样的注册后,初始的filter表,就被添加至表的链表中了,链表首部是全局变量ipt_tables。
同样地,在NAT表的初始化函数ip_nat_rule_init中,有:
ret = ipt_register_table(&nat_table, &nat_initial_table.repl);
表mangle的初始化函数init中,有:
ret = ipt_register_table(&packet_mangler, &initial_table.repl);
注册表的钩子函数
向内核注册了表后,下一步就是注册表的Hook点对应的钩子函数:
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,
},
};
/* Register hooks */
ret = nf_register_hook(&ipt_ops[0]);
ret = nf_register_hook(&ipt_ops[1]);
ret = nf_register_hook(&ipt_ops[2]);
可以看到NF_IP_LOCAL_IN和NF_IP_FORWARD的钩子函数是ipt_hook,NF_IP_LOCAL_OUT的钩子函数是ipt_local_out_hook,它们的优先级都是NF_IP_PRI_FILTER。
OK,接下来看nf_register_hook (netfilter.c)函数是如何实现钩子函数的注册的:
int nf_register_hook(struct nf_hook_ops *reg)
{
struct list_head *i;
spin_lock_bh(&nf_hook_lock);
list_for_each(i, &nf_hooks[reg->pf][reg->hooknum]) {
if (reg->priority < ((struct nf_hook_ops *)i)->priority)
break;
}
list_add_rcu(®->list, i->prev);
spin_unlock_bh(&nf_hook_lock);
synchronize_net();
return 0;
}
list_for_each 函数遍历当前待注册的钩子的协议pf及Hook类型所对应的链表,其首地址是&nf_hooks[reg->pf][reg->hooknum],如果当前待注册钩子的优先级小于匹配的的节点的优先级,则找到了待插入的位置,也就是说,按优先级的升序排列。
list_add_rcu把当前节点插入到查到找的适合的位置,这样,完成后,所以pf协议下的hooknum类型的钩子,都被注册到&nf_hooks[reg->pf][reg->hooknum]为首的链表当中了。
这样,我们就在内核中注册了filter、nat和mangle三个表,并为每个表注册了相应的钩子函数,通过在内核TCP/IP堆栈的各个关键数据转发节点调用NF_HOOK宏,进而就可以调用执行我们注册的钩子函数来进行数据包处理,这就是整个Netfilter框架。