本文中使用的工具为lxr,只注重整个大的流程,不关注其中的细节和精确的原理.内核版本为2.6.38.5
netfilter框架从NF_HOOK这个宏进入,做一系列的工作之后返回.这里选择ip_input.c:443中的 NF_HOOK(NFPROTO_IPV4,NF_INET_PRE_ROUTING,skb,dev,NULL,ip_rcv_finish);开始分析.NF_HOOK中第一个参数为协议类型,目前支持的协议有6个,从netfilter.h:55开始,第二个参数为hook点,目前支持5个值,从netfilter.h:46开始,第三个参数为通过的数据包,第四个参数为进入设备,第五个参数为出口设备,因为选择ip_input位置,所以出口设备为空,第六个参数为回调函数,这里是ip_rcv_finish
NF_HOOK直接调用NF_HOOK_THRESH,后面加了一个参数值是INT_MIN,最小的整数值.这个数字的用处后面会看到.NF_HOOK_THRESH又直接调用nf_hook_thresh,返回值如果为1,则调用回调函数,本例中就是上面的ip_rcv_finish.如果不关注nf_hook_thresh中的调试功能的四行代码,这个函数又直接调用了nf_hook_slow,再不出现真正的函数我就要烦了
171 elem = &nf_hooks[pf][hook];
172 next_hook:
173 verdict = nf_iterate(&nf_hooks[pf][hook], skb, hook, indev,
174 outdev, &elem, okfn, hook_thresh);
175 if (verdict == NF_ACCEPT || verdict == NF_STOP) {
176 ret = 1;
177 } else if ((verdict & NF_VERDICT_MASK) == NF_DROP) {
178 kfree_skb(skb);
179 ret = -(verdict >> NF_VERDICT_BITS);
180 if (ret == 0)
181 ret = -EPERM;
182 } else if ((verdict & NF_VERDICT_MASK) == NF_QUEUE) {
183 if (!nf_queue(skb, elem, pf, hook, indev, outdev, okfn,
184 verdict >> NF_VERDICT_BITS))
185 goto next_hook;
186 }
跳过nf_hook_slow函数中锁的那几行,可以看到这个函数的功能是,根据pf和hook的值(上面的协议类型和hook点值,各种可能的组合为5*6=30种)从nf_hooks中得到一个链表.然后调用nf_iterate函数执行函数,根据返回值操作,如果返回NF_ACCEPT或者NF_STOP,都返回1,即NF_HOOK最外层的回调函数需要调用.如果返回值为NF_QUEUE,则调用nf_queue函数,如果失败,则返回再次调用nf_iterate.这里关注流程,不进去看nf_queue函数,进入nf_iterate
nf_iterate逐个调用链表中的函数,并根据返回值做出不同操作,这里的hook_thresh就是刚才的MIN_INT,相当于调用门槛,这里用最小值即每个函数都调用,别处可以使用较大的门槛
nf_hooks是一个list_head结构,这里可以看出它其实是一个nf_hook_ops结构,其成员用pf,hooknum和priority,以及回调函数hook.这就是整个调用的流程
插入数据的方法有两种,一种是直接插入一个nf_hook_ops,另一种是插入一个表xt_table,内核中只有极少的部分直接使用,用法特别简单,可以参考/net/decnet/netfilter/dn_rtmsg.c.剩下的模块都使用了表,可见直接使用nf_hook_ops把模块插入到nf_hooks中是一个不太礼貌的行为,而且使用表可以和用户态的iptables很方便地通信,是值得鼓励的行为
使用表的方式的模块很多,包括iptable_filter.c,arptable_filter.c,iptable_security.c,iptable_mangle.c和iptable_raw.c.这里分析最常用的iptable_filter.c,文件只有100多行,只分析netfilter相关的内容,在iptable_filter_init中只有一个需要看的函数xt_hook_link.它的参数是一个xt_table结构体和一个回调函数iptable_filter_hook,这个函数只做了检查工作,具体工作交给了ipt_do_table函数,所有使用表的模块都可以使用这个函数.xt_table结构体中pf还是pf,priority还是priority,hooknum变成了valid_hooks,可见表可以在多个hook点上作用.还有一个private,是xt_table_info结构体,应该是存放具体的表格信息.进去看注册函数xt_hook_link
这个函数用汉明距离算了hooks的总数,然后申请了足够多的nf_hook_ops,然后注册.这些nf_hook_ops回调函数的参数都是相同的,只是位置不同,在回调的时候都调用相同的iptable_filter_hook,然后在ipt_do_table中根据挂载点不同可以进行不同的操作,表的注册过程到此结束
其实xt_table更强大的功能在于可以和用户态程序(iptables)方便地通信,而且它的match,target方法也使得添加规则的方法无比灵活,多个match和target可以组合出很多规则,非常方便(比较遗憾的是,它的功能还是不太能满足我的需求,要不然也不用看这个了…)
我们来看xt_limit.c中的match相关的内容,target是类似的.
这个模块注释中讲使用了simple token bucket filter算法,有兴趣的时候可以参考维基,这里只分析netfilter部分,这样以来其实只需要关注一下xt_register_match就可以了.另外它的xt_match结构中的match叫做limit_mt,这个函数以sk_buff指针为参数,即通过的包,和另外一个xt_action_param,这个参数是各个模块使用的,里面定义的一个matchinfo和targetinfo,用来保存私自数据.其他的参数由table框架外的函数填写,是对包的一些重要信息的描述.xt_register_match出奇地简单,它把match插入到xt这个全局变量里就完事了
这样做非常合理,正是策略机制分离的设计方法的体现,match和target只是提供机制,那么保存在这个链表里面就足够了.至于具体的策略,则由上层的iptables添加.支持iptables功能的数据在xt_table_info这个结构里,它是xt_table结构的private成员指向的内容,但在刚才iptable_filter.c中初始化中并没有private的信息,回头再去看看这个人有100多行的文件
在xt_hook_link之前还有一个函数,register_pernet_subsys,这个函数的功能是注册proc子系统,在用户态/proc部分.但它有一个参数叫iptable_filter_net_ops,只是两个初始化和既出函数,初始化函数为iptable_filter_net_init,跟xt_table_info相关的工作可能就是在这个函数中完成的.它先根据packet_filter结构申请了一个ipt_replace结构,然后又根据ipt_replace注册了一个table.然后释放了ipt_replace,工作完成
ipt_alloc_initial_table参数是一个xt_table,返回一个ipt_replace,这不是一个函数,而是一个宏,所以宏里面直接使用了参数名称,在”函数”表面看来没有使用参数.首先申请了一个结构体,里面又包含三个结构体,分别是ipt_replace,ipt_standard,ipt_error.然后把参数里的name复制到repl的name中.然后是一系列的是初始化,然后返回.不过…返回的是ipt_replace结构,这里其实是上面说的三个.原因在文件上面的注释里…也就是说ipt_standard和ipt_error都做为ipt_replace中最后一个参数entries来使用了.然后进入ipt_register_table函数
ipt_register_table为每个cpu都申请了一个xt_table_info,呃,原因不明.然后进入translate_table函数,然后把repl中的信息复制到xt_table_info中.并做了一系列的检查,最后,把信息复制到每个cpu中.netfilter在软中断中调用,虽然同时只能在一个cpu上执行,但每个cpu都有自己的一份数据,所以里面的数字设置要注意,总的数目应该是设置数目和cpu数量的乘积(以上想法都是猜测的)
然后调用xt_register_table,这个函数将private赋值为xt_table_info.到这里,match和target的注册都分析完毕.还剩最后一个,添加规则.
ip_tables.c文件的初始化函数为ip_tables_init.它注册了内置的规则.然后设置添加了一个setsockopt接口选项,提供一个回调函数,可见,iptables是调用setsockopt来实现规则向内核传递的.那么需要分析的只有一个函数do_ipt_set_ctl
do_ipt_set_ctl函数接收用户态的数据之后,把它们并入到内核的规则中去.首先检查了超级用户权限.之后一个switch到两个函数,分别是do_replace替换原来的规则和do_add_counters添加新的规则.
先看只添加的内容,do_add_counters函数,用户态传入的数据是一个xt_counters_info结构,其中有表的名字name,规则的数目counters,和最后的规则xt_counters,是一个零数组.xt_counters包含两个成员,pcnt和bcnt,分别是包的个数和比特大小.然后使用vmalloc申请内存,这个函数可以申请大块内存,然后才拷贝真正的数据.根据名称从寻找对应表,如果不存在就返回错误,因为表总共只有数个,这里用线性查找.添加了entry的计数和比特数目,do_replace是类似的工作原理
ipt_do_table函数先做了一些准备工作,把一些必须数据计算出来,然后根据pf和hooknum寻找match,找到之后执行函数,并根据结果来决定是否要执行相应的target