本文共 35058 字,大约阅读时间需要 116 分钟。
网络地址转换:NAT
Netfitler为NAT在内核中维护了一张名为nat的表,用来处理所有和地址映射相关的操作。诸如filter、nat、mangle抑或raw这些在用户空间所认为的“表”的概念,在内核中有的是以模块的形式存在,如filter;有的是以子系统方式存在的,如nat,但它们都具有“表”的性质。因此,内核在处理它们时有很大一部操作都是相同的,例如表的初始化数据、表的注册、钩子函数的注册等等。关于NAT表的初始化模板数据和表的注册流程并不是本文的重点,大家可以对照第四篇博文中filter表的相关分析来研究。本文还是侧重于从整体上对整个NAT子系统的设计思想和原理进行,当然,有时间我还是会简单和大家分析一NAT表的东西。因为最近确实太忙了,本来想着在四月份结束这个系列,无奈一转眼就晃到了五月份,做IT的娃,都不容易啊!
通过前面的几篇文章我们已经知道,NAT的设计是独立于连接跟踪系统的,即连接跟踪是NAT的基础框架,我们还了解到连接跟踪不会修改数据包,它只是负责维护数据包和其所属的业务会话或数据连接状态的相关信息而已。连接跟踪最终是被iptables模块所使用的,它所定义的那些状态信息如NEW、ESTABLISHED、RELEATED等等,NAT统统不用关心。
根据前面的hook函数挂载图我们可以清晰的知道,对于那些需要被本机转发的数据包,注册在NF_IP_PRE_ROUTING点的ip_nat_in ()函数完成对其目的地址转换的操作,注册在NF_IP_POST_ROUTING点的ip_nat_out()函数完成源地址转换任务。如果在编译Linux内核源码时打开了CONFIG_IP_NF_NAT_LOCAL选项,则注册在NF_IP_LOCAL_OUT和NF_IP_LOCAL_IN点的hook函数就会工作,最常见的用法的是用NF_IP_LOCAL_OUT点的ip_nat_local_fn()函数来改变本机发出报文的目的地址。至于注册在NF_IP_LOCAL_IN点的ip_nat_fn()函数一般不会起作用,只是当数据包到达该HOOK点后会例行被调用一下。因为,NAT的所有规则只可能被配置到nat表的PREROUTING、POSTROUTING和OUTPUT三个点上,一般也很少有人去修改那些路由给本机的报文的源地址。
NAT 的分类如下图所示:相信大家在看iptables用户指南都见过这么一句解释:
只有每条连接的第一个数据包才会经过nat表,而属于该连接的后续数据包会按照第一个数据包则会按照第一个报所执行的动作进行处理,不再经过nat表。Netfilter为什么要做这个限制?有什么好处?它又是如何实现的?我们在接下来的分析中,将一一和大家探讨这些问题。
在ip_nat_rule.c文件中定义了nat表的初始化数据模板nat_table,及相应的target实体:SNAT和DNAT,并将其挂在到全局xt[PF_INET].target链表中。关于NAT所注册的几个hook函数,其调用关系我们在前几篇博文中也见过:
因此,我们的核心就集中在ip_nat_in()上。也就是说,当我们弄明白了ip_nat_fn()函数,你就差不多已经掌握了nat的精髓。ip_nat_in()函数定义定在ip_nat_standalone.c文件里。连接跟踪作为NAT的基础,而建立在连接跟踪基础上的状态防火墙同样服务于NAT系统。
关于ip_nat_fn()函数 我们还是先梳理整体流程,以便大家对其有一个宏观整体的把握,然后我们再来分析其实现细节。这里需要大家对连接跟踪的状态跃迁有一定了了解。从流程图可以看出,牵扯到的几个关键函数都土黄色标注出来了。ip_nat_setup_info()函数主要是完成对数据包的连接跟踪记录ip_conntrack对象中相应成员的修改和替换,而manip_pkt()中才是真正对skb里面源/目的地址,端口以及数据包的校验和字段修改的地方。
目的地址转换:DNAT
DNAT主要适用于将内部私有地址的服务发布到公网的情形。情形如下:服务器上架设了Web服务,其私有地址是B,代理防火墙服务器有一个公网地址A。想在需要通过A来访问B上的Web服务,此时就需要DNAT出马才行。根据前面的流程图,我们马上杀入内核。
当client通过Internet访问公网地址A时,通过配置在防火墙上的DNAT将其映射到了对于私网内服务器B的访问。接下来我们就分析一下当在client和server的交互过程中架设在防火墙上NAT是如何工作。
还是看一下hook函数在内核中的挂载分布图。
在PREROUTING点当一个skb被连接跟踪过后,那么skb->ctinfo和skb->nfct两个字段均被设置了值。在接下来的分析中,对那些梢枝末节的代码我们都将不予理睬。盗个图:
这里牵扯到一个变量ip_conntrack_untracked,之前我们见过,但是还没讨论过。该变量定义在ip_conntrack_core.c文件里,并在ip_conntrack_init()函数进行部分初始化:
atomic_set(&ip_conntrack_untracked.ct_general.use, 1); set_bit(IPS_CONFIRMED_BIT, &ip_conntrack_untracked.status); |
同时,在ip_nat_core.c文件里的ip_nat_init()函数中又有如下设置:
ip_conntrack_untracked.status |= IPS_NAT_DONE_MASK; |
该变量又是何意呢?我们知道iptables维护的其实是四张表,有一张raw不是很常用。该表以-300的优先级在PREROUTING和LOCAL_OUT点注册了ipt_hook函数,其优先级要高于连接跟踪。当每个数据包到达raw表时skb->nfct字段缺省都被设置成了ip_conntrack_untracked,所以当该skb还没被连接踪的话,其skb->nfct就一直是ip_conntrack_untracked。对于没有被连接跟踪处理过的skb是不能进行NAT的,因此遇到这种情况代码中直接返回ACCEPT。
从上面的流程图可以看出,无论是alloc_null_binding_confirmed()、alloc_null_binding()还是ip_nat_rule_find()函数其本质上最终都调用了ip_nat_setup_info()函数。
ip_nat_setup_info()函数:
该函数中主要完成了对连接跟踪记录ip_conntrack.status字段的设置,同时根据可能配置在nat表中的DNAT规则对连接跟踪记录里的响应tuple进行修改,最后将该ip_conntrack实例挂载到全局双向链表bysource里。
在连接跟踪系统里根据skb的源/目的IP分别已经构建生成初始tuple和响应tuple,我们通过一个简单的示意图来回顾一下其流程,并加深对ip_nat_setup_info()函数执行过程的理解。在图1中,根据skb的源、目的IP生成了其连接跟踪记录的初始和响应tuple;
在图2中,以初始tuple为输入数据,根据DNAT规则来修改将要被改变的地址。这里是当我们访问目的地址是A的公网地址时,DNAT规则将其改成对私网地址B的访问。然后,计算被DNAT之后的数据包新的响应。最后用新的响应tuple替换ip_conntrack实例中旧的响应tuple,因为数据包的目的地址已经被改变了,所以其响应tuple也必须跟着变。
在图3中,会根据初始tuple计算一个hash值出来,然后以ip_conntrack结构中的nat.info字段会被组织成一个双向链表,将其插入到全局链表bysource里。
最后,将ip_conntrack.status字段的IPS_DST_NAT和IPS_DST_NAT_DONE_BIT位均置为1。
这里必须明确一点:在ip_nat_setup_info()函数中仅仅是对ip_conntrack结构实例中相关字段进行了设置,并没有修改原始数据包skb里的源、目的IP或任何端口。
ip_nat_packet()函数的核心是调用manip_pkt()函数:
在manip_pkt()里主要完成对数据包skb结构中源/目的IP和源/目的端口的修改,并且修改了IP字段的校验和。从mainip_pkt()函数中返回就回到了ip_nat_in()函数中(节选):
ret = ip_nat_fn(hooknum, pskb, in, out, okfn); if (ret != NF_DROP && ret != NF_STOLEN&& daddr != (*pskb)->nh.iph->daddr) { dst_release((*pskb)->dst); (*pskb)->dst = NULL; } return ret; |
正常情况下返回值ret一般都为NF_ACCEPT,因此会执行if条件语句,清除skb原来的路由信息,然后在后面协议栈的ip_rcv_finish()函数中重新计算该数据包的路由。
在数据包即将离开NAT框架时,还有一个名为ip_nat_adjust()的函数。参见hook函数的挂载示意图。该函数主要是对那些执行了NAT的数据包的序列号进行适当的调整。如果调整出错,则丢弃该skb;否则,将skb继续向后传递,即将到达连接跟踪的出口ip_confirm()。至于,ip_confirm()函数的功能说明我们在连接跟踪章节已经深入讨论了,想不起来的童鞋可以回头复习一下连接跟踪的知识点。
前面我们仅分析了从client发出的第一个请求报文到server服务器时,防火墙的处理工作。紧接着我们顺着前面的思路继续分析,当server收到该数据包后回应时防火墙的处理情况。
server收到数据包时,该skb的源地址(记为X)从未变化,但目的地址被防火墙从A改成了B。server在响应这个请求时,它发出的回应报文目的地址是X,源地址是自己的私有地址B。大家注意到这个源、目的地址刚好匹配被DNAT之后的那个响应tuple。
当该回应报文到达防火墙后,首先是被连接跟踪系统处理。显而易见,在全局的连接跟踪表ip_conntrack_hash[]中肯定可以找到这个tuple所属的连接跟踪记录ip_conntrack实例。关于状态的变迁参见博文八。
然后,该回应报文到达NAT框架的ip_nat_in()函数,流程和前面一样,但处理方式肯定不同。我们还是先看一下截止到目前为止,这条连接跟踪结构图:直接跳到ip_nat_packet()函数里,当netlink通知机制将连接跟踪状态由NEW变为REPLY后,此时dir=1,那么根据初始tuple求出原来的响应tuple:源地址为A,目的之为X。此时,server的响应报文,源地址为私有网段B,目的地址为X。路由寻址是以目的地址为依据,防火墙上有直接到client的路由,所以响应报文是可以被client正确收到的。但是,但可是,蛋炒西红柿,对于UDP来说client收到这样的回复没有任何问题的,但是对于TCPU而言确实不行的。这就引出我们接下来将要讨论的SNAT。
未完,待续…源地址转换:SNAT
SNAT 主要应用于下列场景:这种情况下,我们只有一个公网地址A,而又有三台主机需要同时上网,这时就需要SNAT了。它的主要作用是将那些由私网发来的数据包skb的源地址改成防火墙的公网地址A,这是因为目的主机在响应源地址为私网地址的数据包时,私网地址不能在网络上路由的缘故。
SNAT仅可以在LOCAL_OUT和POSTROUTING点生效,这也说明了为什么用户空间的iptables命令在配置SNAT规则时只能配置到nat表的OUTPUT和POSTROUTING链的缘由。
我们现在假设的情形是:其他四个HOOK点都没配任何规则,且内置链的缺省处理策略为ACCEPT,然后在防火墙上配置了一条SNAT规则,私网地址B1要访问公网地址C的情况。
和前面DNAT类似,当第一个由B1发往C的数据包到达POSTROUTING点时,在连接跟踪阶段就已经为该连接建立好了ip_conntrack实例并进行过适当的初始化。SNAT主要是在ip_nat_out()函数中完成,而该函数本质上也调用了ip_nat_fn(),所以流程和DNAT一样,但执行的操作有所差别。
还是先回顾一下 ip_nat_fn() 的流程图:注意,当执行ip_nat_out()函数时,该skb已经被正确路由过了。此时,在ip_nat_fn()里执行的是ip_nat_rule_find()分支,然后进入ip_nat_setup_info()函数中。
接下来我们简单说一下get_unique_tuple()函数。
如果属于某条连接的数据包之前已经被执行过 NAT 了,则其连接跟踪记录会被添加到 bysource 链表中。对于 SNAT 操作,如果是第一个数据包,其流程和 DNAT 非常相似,在函数 find_best_ips_proto() 中完成对临时 tuple 的源地址的修改。然后,以修改后的 tuple 计算其响应 tuple ,最终用该响应 tuple 替换掉连接跟踪记录中原来的响应 tuple 。替换的结果是:此时,连接跟踪记录中响应tuple的源地址已经被替换成防火墙的公网地址A了。等会儿当服务器C回应时,它所发出的报文目的地址就是A,这样防火墙就可以正确接收到服务器C的回应报文。最后,会根据该连接跟踪记录实例的初始tuple来计算一个hash值,然后将其插入到bysource里,并对ip_conntrack.status状态进行适当设置。
和DNAT类似,最后也是在manip_pkt()函数中完成对skb源地址、IP校验和进行修改。对skb里端口的修改也是在manip_pkt()里完成的。
OK,关于SNAT我们第一阶段的分析就完成了,接下来我们来看一下当服务器C收到这个请求报文后,在对其响应的后续流程里防火墙是如何实现所谓的自动De-SNAT功能。
服务器的响应报文其源地址为自己的公网地址C,目的地址为防火墙的公网地址A。当该响应报文到达防火墙后,连接跟踪系统可以准确地识别该数据包所欲的连接跟踪记录,因为存在一个源地址是C,目的地址是A的响应tuple(如上图所示)与其匹配。该响应数据包还是会先到达PREROUTING点的ip_nat_in()函数,因为没有配置任何DNAT规则,同时ct.status字段又设置了IPS_SRC_NAT和IPS_SRC_NAT_DONE_BIT标志位,所以在进入到ip_nat_in()函数的ip_nat_fn()里时就直接调用ip_nat_packet()接口。
在ip_nat_packet()中以连接跟踪记录的初始tuple计算原来旧的响应tuple:源地址是C,目的地址是B1。因为在PREROUTING点上要做DNAT,所以此时skb中的目的地址A就被改成了原来的响应tuple中的目的地址B1了。后面的流程和DNAT完全一样。
当该响应报文来到POSTROUTING点时,又调用ip_nat_out(),和前面分析SNAT一阶段时的流程一样。
现在我们就明白了,De-SNAT功能是基于manip_pkt()函数实现的。在结尾之际,我们来解释一下上篇博文中关于NAT的一段描述:“只有每条连接的第一个数据包才会经过nat表,而属于该连接的后续数据包会按照第一个数据包则会按照第一个报所执行的动作进行处理,不再经过nat表”。
从用户空间的iptables规则来看,每条规则都有一个counter计数器,该计数器记录的是被该规则成功匹配了数据包的数目。而内核中对该计数器的修改是在ipt_do_table()函数。从ip_nat_fn()函数的执行流程可以看出,当连接跟踪记录项被设置了IPS_SRC_NAT_DONE_BIT状态位,或者连接跟踪的状态不再是IP_CT_NEW状态时,ipt_do_table()函数就不再被调用了,反应在用户空间所看到的直观现象就是,nat表的规则计数器不再增长了。
基于连接跟踪的 NAT ,其特殊之处就在于初始和响应 tuple 不再一致了,而这也是 NAT 得以正确运行的关键所在。通过如下这张图,让大家再复习一下 NAT 的初始和响应 tuple 之间的关系:至此,Linux中Netfilter框架下的nat子系统我们就全部学习完了。用了十个章节基本将Netfilter框架的各个功能子模块、架构、原理、流程等作了初步简单的分析了解。由于知识有限,本人的分析难免存在疏漏,还请各位大侠不吝指正。在接下来后续的文章中,主要探讨以下主题:
1、 用户空间的iptables是怎样识别传递给它的每个参数?
2、 通过iptables配置的每条规则是如何进到内核里的?它们又有什么关系?
3、 内核是如何判断数据包到底匹配还是不匹配某条具体规则?
4、 如何自己动手扩展iptables的功能模块?
未完,待续…
预备知识:
part1: 初见getopt_long()
在分析iptables源码时,作为命令解析的核心函数getopt_long()不得不提。随便百度或google搜索关于该函数的介绍有很多例子和解释,这里我只举一例,目的是让大家了解传递给iptables命令的每个参数是如何被正确识别并处理的。
getopt_long(int argc,char * const argv[],const char *optstring,const struct option *longopts,int *longindex)
参数说明:
argc和argv来自main函数的输入;
optstring:表示可以接受的参数。可以是下列值:1.单个字符,表示选项;2.单个字符后接一个冒号“:”表示该选项后必须跟一个参数。参数紧跟在选项后或者以空格隔开,该参数的指针赋给optarg。3.单个字符后跟两个冒号,表示该选项后必须跟一个参数。参数必须紧跟在选项后不能以空格隔开。该参数的指针赋给optarg。(这个特性是GNU的扩展)。例如,"a:b:cd",表示可以接受的参数选项是a,b,c,d,其中a和b参数后面跟有参数值。
longopts:是一个结构类型,描述如下:
struct option { const char *name; //name表示的是长参数名 int has_arg; //0-无参;1-一定要有;2-可有可无 int *flag; //用来决定,getopt_long()的返回值到底是什么。 //如果flag是null,则函数会返回与该项option匹配的val值。int val; //和flag联合决定返回值
}
在iptables的do_command()命令解析函数中,见到最多的就是optarg和optind。
optarg: 如果字符串optstring中某个选项后面需要跟参数,该参数值一般保存在optarg中;
optind: 该参数比较费神,输入参数列表不同,其取值也不一样。
说了半天估计大家都快晕了,还是通过例子来说明这两个值随着输入参数的不同其变化情况吧。
#include <stdio.h> #include <getopt.h> char *l_opt_arg; char* const short_options = "nb:ls:"; struct option long_options[] = { { "name", 0, NULL, 'n' }, { "bf_name", 1, NULL, 'b' }, { "love", 0, NULL, 'l' }, { "speed", 1, NULL, 's' }, { 0, 0, 0, 0}, };
int main(int argc, char *argv[]) { int c; printf("init otpind=%d\n",optind); while((c = getopt_long (argc, argv, short_options, long_options, NULL)) != -1) { printf("option=%c,optind=%d,optarg=%s\n",c,optind,optarg); printf("args[%d]=%s\n",optind,argv[optind]); switch (c) { case 'n': printf("My name is XL.\n"); break; case 'b': printf("His name is ST.\n"); break; case 'l': printf("Our love is ok!\n"); break; case 's': printf("SHit of son.\n"); break; } } return 0; } |
该测试程序接受的可选参数为-n -b -l -s,其中b和s选项后面要跟值。
如果我们执行./test -n -b boy -l -s son
optind依次取值为1、2、4、5、7。默认值就是1,当解析-n时,因为发现-n不需要值,所以当调用了getopt_long后,optind自动加1,指向-b选项。而-b是需要参数的,那么跟在-b后面的一定是它的值,所以当解析-b时,optind自动跳到下一个选项-l所在位置。同样-s也需要跟参数,那么当解析-l时optind就自动跳到-s所在的位置了,即5。
如果我们执行./test -n --b=boy -l -s son
这样的格式,optind依次为1、2、3、4、6。大家基本已经可以看出些眉目了吧。因为-b参数用长参格式加等号的赋值方式,所以optind的移动稍微有些变化。但它终归可以正确识别传给它的所有命令行参数及其格式。
如果我们执行./test -nl -b boy -s son
optind依次取值1、1、2、4、6。第一个选项是nl组合项,而且这两个选项都不需要跟参数。
关于getopt_long函数的更多用法参见man帮助手册。自己再对上面这个程序摸索摸索体会要更深刻些。
part2:iptables的自动加载模块原理
无论是match还是target,在用户空间都有其对应的so库文件,关于动态库大家可以参阅读我的另一篇博文《》。这里我们注意到一点的就是无论是诸如libxt_tcp.c这样的协议模块,还是libxt_limit.c这样的match模块,又抑或libipt_REJECT.c这样的target模块,每个模块中都有一个名为_init()的函数。为什么我们的自己平时开发so库时,怎么没见过这个家伙?大家可能会有这疑问。接下来我们就来跟您抽丝剥茧,步步深入,看看它到底是何方妖孽。
iptables在加载动态库时用的是dlopen()函数,在这篇博文中我有介绍。_init()定义在xtables.h中,是共享库用来初始化全局变量和对象用的,其定义如下:
#define _init __attribute__((constructor)) my_init
用__attribute__((constructor))来定义的函数,表示函数是构造函数,在main执行之前被调用;相应的用__attribute__ ((destructor))析构函数,在main退出时执行。void _init(void)就相当于是__attribute__((constructor)) _INIT(void),其实不管函数名定义成什么都会被执行到。
在iptables中当我们调用dlopen函数来加载动态库时,率先执行每个动态库里的_init()函数,而该函数要么是将该match注册到全局链表xtables_matches里,或者是将target注册到全局链表xtables_targets中。
iptables的命令解析流程
这里我们仅以ipv4协议为例进行分析。iptables-1.4.0.tar.gz源代码中,iptables命令的入口文件为iptables-standalone.c,其中主函数为main或者iptables_main。主函数中,所作的事情也很明了,其流程如下:当前,用户空间的iptables工具的绝大多数模块都是以动态共享库so的形式。使用动态库的优点也是显而易见的:编译出来的iptables命令比较小,动态库方式使得对于iptables的扩充非常方便。如果你非要去研究一下init_extensions函数的话,那么可以在iptables源码包的extensions/Makefile文件里找点思路。这里,我不会对其进行分析。
命令行参数解析do_command()【位于iptable.c文件中】
该函数是iptables用于解析用户输入参数的核心接口函数,其函数原型为:
int do_command(int argc, char *argv[], char **table, iptc_handle_t *handle);
argc和argv是由用户传递过来的命令行参数;
table所操作的表名,对应命令行就是-t参数后面的值,如果用户没有指定-t参数时,默认为filter表;
handle这个结构比较重要,它用于保存从内核返回的由table所指定的表的所有信息,后续对表及其其中的规则操作时都用的该变量;
前面我们在分析netfilter的时候提到过,用户空间和内核空间在表示match以及target时采用了不同的结构体定义。用户空间的match结构体定义为:
struct xtables_match #define iptables_target xtables_target { struct xtables_match *next; … void (*help)(void); /* Initialize the match. */ void (*init)(struct xt_entry_match *m); … /* Ignore these men behind the curtain: */ unsigned int option_offset; struct xt_entry_match *m; #内核中的match结构 unsigned int mflags; … }; |
该结构是iptables在加载它所支持的所有match模块的时候所用到的结构体,例如time匹配模块、iprange匹配模块等。也就是说,如果你要开发自己的用户空间match的话,那么你必须实例化上面这样一个结构体对象,然后实现它相应的方法,诸如init、help、parse等等。
真正用在我们所配置的iptables规则里的匹配条件,是由下列类型表示:
struct xtables_rule_match #define iptables_rule_match xtables_rule_match { struct xtables_rule_match *next; struct xtables_match *match; unsigned int completed; }; |
可以看到,xtables_rule_match是将xtables_match组织成了一个链表而已。这也正和我们的意愿,因为一条规则里有可能会有多个match条件,而在解析的时候我们只要将我们规则里所用的match通过一个指针指向iptables目前所支持的那个模块,在后面的使用过程中就可以直接调用那个match模块里的所有函数了。这样即提高的访问效率,又节约了系统内存空间。
同样的,用户空间的target也类似,留给大家自己去研究。
iptables最常用的命令格式无非就是显示帮助信息,或者操作规则,例如:
【帮助信息格式】
iptables [-[m|j|p] name ] -h 显示名为name的match模块(m)、target模块(j)或协议(p)的详细帮助信息。
OK,我们以下面的规则为例,和大家探讨一下iptables对其的解析流程。
iptables –A INPUT –i eth0 –p tcp --syn –s 10.0.0.0/8 –d 10.1.28.184 –j ACCEPT
在博文三中,我们知道内核中用于表示一条规则的数据结构是struct ipt_entry{}类型,那么iptables对于输入给它的所有参数最终也要变成这样的格式。而我们在阅读iptables源码时发现,它确实在do_command()函数开始部分定义了一个struct ipt_entry fw;后面当iptables解析传递给它的输入参数时,主要做的事情,就是对该结构体相关成员变量的初始化填充。闲话不多说,let's rock。
(1)、命令控制解析:-A INPUT
iptables –A INPUT –i eth0 –p tcp --syn –s 10.0.0.0/8 –d 10.1.28.184 –j ACCEPT
对于“ADRILFZNXEP”这些控制命令来说,其核心处理函数为add_command()函数。
该函数主要将命令行的控制参数解析出来,然后赋值给一个位图变量command,该变量的每一位bit表示一种操作。add_command()的函数原型定义如下(iptables.c):
static void add_command(unsigned int *cmd, const int newcmd, const int othercmds, int invert)
参数说明:
cmd:用于保存控制参数解析结果的位图标志变量;
newcmd:用户所输入的控制变量,是一些预定义的宏,定义在iptables.c文件中,如下:
#define CMD_NONE 0x0000U #define CMD_INSERT 0x0001U #define CMD_DELETE 0x0002U #define CMD_DELETE_NUM 0x0004U #define CMD_REPLACE 0x0008U #define CMD_APPEND 0x0010U #define CMD_LIST 0x0020U #define CMD_FLUSH 0x0040U #define CMD_ZERO 0x0080U #define CMD_NEW_CHAIN 0x0100U #define CMD_DELETE_CHAIN 0x0200U #define CMD_SET_POLICY 0x0400U #define CMD_RENAME_CHAIN 0x0800U |
othercmd:在上面这11个控制参数中,只有CMD_ZERO需要辅助额外参数,因为从iptables -Z chainname的执行结果来看,它最后还会输出清空后的链的实际情况。因此,当用户的iptables命令中有-Z参数时,cmd默认的会被附加一个CMD_LIST特性。其他10个控制参数时,othercmd参数均为CMD_NONE。
invert:表示命令中是否有取反标志“!”。因为这11个控制参数是没有取反操作的,因此,这个值均为FALSE(即0)。
当解析完iptables -A INPUT … 后,command=0x0010U,chain=“INPUT”。然后将invert=FALSE,重新进入while循环,解析剩下的参数。
(2)、解析接口:-i eth0
iptables –A INPUT –i eth0 –p tcp --syn –s 10.0.0.0/8 –d 10.1.28.184 –j ACCEPT
注意前面讲解的关于getopt_long()函数在执行过程中两个关键参数的值及其变化情况。当解析接口的时候optarg=“eth0”,optind=indexof(-p)。
check_inverse(optarg, &invert, &optind, argc);函数用于判断接口是否有取反标志,如果有取反标志,则将invert=TRUE,同时optind++,然后它指向后面的接口名,并返回TRUE;如果没有,则直接返回FALSE。
在接下来执行set_option(&options, OPT_VIANAMEIN, &fw.ip.invflags,invert);同样的,options也是一个位图标志变量,其取值分别如下(定义在iptables.c文件中):
#define OPT_NONE 0x00000U #define OPT_NUMERIC 0x00001U #define OPT_SOURCE 0x00002U #define OPT_DESTINATION 0x00004U #define OPT_PROTOCOL 0x00008U #define OPT_JUMP 0x00010U #define OPT_VERBOSE 0x00020U #define OPT_EXPANDED 0x00040U #define OPT_VIANAMEIN 0x00080U #define OPT_VIANAMEOUT 0x00100U #define OPT_FRAGMENT 0x00200U #define OPT_LINENUMBERS 0x00400U #define OPT_COUNTERS 0x00800U #define NUMBER_OF_OPT 12 |
然后根据check_inverse()函数解析出来的invert的值来设置fw.ip.invflags相应的标志位,该值也是个位图标志变量,其可取的值由全局数组inverse_for_options[]来限定(iptables.c):
static int inverse_for_options[NUMBER_OF_OPT] = { /* -n */ 0, /* -s */ IPT_INV_SRCIP, #这六个宏均定义在ip_tables.h文件中 /* -d */ IPT_INV_DSTIP, /* -p */ IPT_INV_PROTO, /* -j */ 0, /* -v */ 0, /* -x */ 0, /* -i */ IPT_INV_VIA_IN, /* -o */ IPT_INV_VIA_OUT, /* -f */ IPT_INV_FRAG, /*--line*/ 0, /* -c */ 0, }; |
执行parse_interface(argv[optind-1],fw.ip.iniface,fw.ip.iniface_mask);将接口名称赋值给fw.ip.iniface,然后再设置该接口的mask。如果接口中没有正则匹配表达式(即“+”),则mask=0xFFFFFFFF。细心的朋友到这里可能就有疑问了:接口名不是保存在optarg中么,为什么要通过argv[optind-1]来获取呢?我们简单分析对比一下:
如果是“-i eth0”,那么optarg和argv[optind-1]的值相同,大家可以通过前面我给的那个demo例子去验证一下;
如果是“-i ! eth0”,情况就不一样了。注意看代码,此时optarg=“!”,而arg[optind-1]才是真正的接口名“eth0”。
(3)、解析协议字段:-p tcp
iptables –A INPUT –i eth0 –p tcp --syn –s 10.0.0.0/8 –d 10.1.28.184 –j ACCEPT
check_inverse(optarg, &invert, &optind, argc); 检查协议字段是否有取反标志
set_option(&options, OPT_PROTOCOL, &fw.ip.invflags,invert); 根据invert的值来设置options和fw.ip.invflags。这和前面的接口解析是类似的。
然后,将协议名称解析成对应的协议号,例如ICMP=1,TCP=6,UDP=17等等。
fw.ip.proto = parse_protocol(protocol);
因为iptables在-p参数后面支持数字格式的协议描述,因此parse_protocol()函数首先尝试去解析数字字符串,将其转换成一个0-255之间的整数。如果转换成功,则将转换结果赋值给fw.ip.proto。如果转换失败,首先检查-p后面的参数是不是“all”。如果是则直接返回,否则调用getprotobyname()函数从/etc/protocols中去解析。这里getprotobyname函数主要根据传递给它的协议名返回一个struct protoent{}结构体的对象(详见man手册)。解析成功则返回;否则,在用户自定义的结构体数组chain_protos[]中去解析,其定义如下:
static const struct pprot chain_protos[] = { { "tcp", IPPROTO_TCP }, { "udp", IPPROTO_UDP }, { "udplite", IPPROTO_UDPLITE }, { "icmp", IPPROTO_ICMP }, { "esp", IPPROTO_ESP }, { "ah", IPPROTO_AH }, { "sctp", IPPROTO_SCTP }, { "all", 0 }, }; |
if (fw.ip.proto == 0&& (fw.ip.invflags & IPT_INV_PROTO))
exit_error(PARAMETER_PROBLEM,"rule would never match protocol");
如果协议类型为“all”并且协议字段-p后面还有取反标志,即-p ! all,表示不匹配任何协议。这样的规则是没有任何意义的,iptables也不允许这样的规则存在,因此会给出错误提示信息并退出。
(4)、解析tcp协议模块的具体控制参数:--syn
iptables –A INPUT –i eth0 –p tcp --syn –s 10.0.0.0/8 –d 10.1.28.184 –j ACCEPT
针对于--syn符号,会跳转到switch语句的default处执行。因为目前还没有解析到target,因此target=NULL。命令行中没有-m,因此matches=NULL,matchp=NULL,m=NULL。
if (m == NULL&& protocol&&
(!find_proto(protocol, DONT_LOAD,options&OPT_NUMERIC, NULL)
|| (find_proto(protocol, DONT_LOAD,options&OPT_NUMERIC, NULL)
&& (proto_used == 0))
)
&& (m = find_proto(protocol, TRY_LOAD,options&OPT_NUMERIC, &matches))) {
这个逻辑条件判断已经很清晰了:
如果命令行中没有-m,但是有-p,并且find_proto执行失败或者执行成功且协议本身还没有被用过proto_used=0,最后我们试图去加载so库之后再去执行find_proto。当第三执行find_proto函数时,会运行如下的代码部分,因为我们这次是以TRY_LOAD方式执行的:
#ifndef NO_SHARED_LIBS if (!ptr && tryload != DONT_LOAD && tryload != DURING_LOAD) { char path[strlen(lib_dir) + sizeof("/.so") + strlen(afinfo.libprefix) + strlen(name)]; sprintf(path, "%s/libxt_%s.so", lib_dir, name); if (dlopen(path, RTLD_NOW) != NULL) /* Found library. If it didn't register itself, maybe they specified target as match. */ ptr = find_match(name, DONT_LOAD, NULL); |
以上代码会将我们的…/libxt_tcp.so库加载到当前进程的运行空间中,并导出相关环境变量,此时tcp的模块在执行dlopen时就已经被挂到xtables_matches链表中了。最后再在find_match()函数(find_proto()函数的内部其实就是调的find_match()而已)里递归的调用一次自己。
第二次递归调用自己时,首先会申请一块大小为struct xtables_match{}的内存空间由变量clone来指向,并将tcp.so模块中的信息保存其中,并设置clone->mflags = 0。然后再申请一块大小为struct xtables_rule_match{}大小的内存空间,由变量newentry来保存,将tcp的so模块的信息赋给结构体的相关成员变量。
for (i = matches; *i; i = &(*i)->next) { #不会执行这个for循环 printf("i=%s\n",(i==NULL?"NULL":i)); if (strcmp(name, (*i)->match->name) == 0) (*i)->completed = 1; } newentry->match = ptr; //就是前面的clone所指向的地址空间。 newentry->completed = 0; newentry->next = NULL; *i = newentry; #因为matches是个二级指针,因此这里的*i即*matches=newentry return ptr; #ptr目前就保存了和tcp模块所有相关的内容,ptr最后返回去会赋给 下面的变量m |
然后回到do_command()中继续执行:
/* Try loading protocol */ size_t size;
proto_used = 1;
printf("Ready to load %s's match\n",protocol);
size = IPT_ALIGN(sizeof(struct ipt_entry_match))+ m->size;
m->m = fw_calloc(1, size); #为内核态的xt_entry_match结构分配存储空间 m->m->u.match_size = size; #整个tcp_match的大小 strcpy(m->m->u.user.name, m->name); set_revision(m->m->u.user.name,m->revision); if (m->init != NULL) m->init(m->m);#调用tcp_init函数初始化内核中的match结构,主要是将xt_entry_match尾部的data数组进行初始化。对TCP来说就是将源、目的端口置为0xFFFF。这并不是重点。
opts = merge_options(opts,m->extra_opts, &m->option_offset); #重点是merge_options操作,将tcp_opts中的数据合并到全局变量opts中去 optind--; continue; #前面说过optind指向当前参数下一个紧挨着的参数的下标。目前只是完成了解析--syn的初始化工作,还并没有对--syn进行解析,因此需要optind--,然后开始解析--syn |
然后程序继续执行while循环,这次依然进入default段进行处理,并进入if (!target||…
只不过这次matches已经不为NULL,因此matchp就可以取到值,matchep即指向了tcp模块。将解析的结果赋给fw结构体的相应成员,并将代表tcp模块的iptables_match赋给m。
if (!target|| !(target->parse(c - target->option_offset,argv, invert,&target->tflags,&fw, &target->t))) { for (matchp = matches; matchp; matchp = matchp->next) { if (matchp->completed) continue; #调用tcp模块的parse函数,即tcp_parse if (matchp->match->parse(c - matchp->match->option_offset,argv, invert, &matchp->match->mflags,&fw, &matchp->match->m)) break; }
m = matchp ? matchp->match : NULL;
if(m==NULL &&…) #就不会再执行这里了 … … |
至此,对--syn的解析就已经完成了。
(5)、解析源、目的地址:-s 10.0.0.0/8 -d 10.1.28.184
iptables –A INPUT –i eth0 –p tcp --syn –s 10.0.0.0/8 –d 10.1.28.184 –j ACCEPT
解析源地址: check_inverse(optarg, &invert, &optind, argc); set_option(&options, OPT_SOURCE, &fw.ip.invflags,invert); shostnetworkmask = argv[optind-1]; #暂存源地址,后面要做进一步分析x.x.x.x/xx
解析目的地址: check_inverse(optarg, &invert, &optind, argc); set_option(&options, OPT_DESTINATION, &fw.ip.invflags,invert); dhostnetworkmask = argv[optind-1]; #暂存目的地址,后面要做进一步分析x.x.x.x/xx |
(6)、解析target:-j ACCEPT
iptables –A INPUT –i eth0 –p tcp --syn –s 10.0.0.0/8 –d 10.1.28.184 –j ACCEPT
首先判断target字串是否合法,jumpto = parse_target(optarg);
然后在xtables_targets全局链表里查找相应的target。因为目前只有标准target,因此最后加载libxt_standard.so库,对应的文件为libxt_standard.c。
static struct xtables_target standard_target = { .family = AF_INET, .name = "standard", .version = IPTABLES_VERSION, .size = XT_ALIGN(sizeof(int)), .userspacesize = XT_ALIGN(sizeof(int)), .help = standard_help, .parse = standard_parse, }; |
我们可以看到标准target(诸如ACCEPT、DROP、RETURN、QUEUE等)是没有init函数和extra_opts变量的。因此,要做的操作只有下面几个:
if (target) { size_t size;
size = IPT_ALIGN(sizeof(struct ipt_entry_target))+ target->size;
target->t = fw_calloc(1, size); #为内核中的xt_entry_target分配存储空间 target->t->u.target_size = size; strcpy(target->t->u.user.name, jumpto); set_revision(target->t->u.user.name,target->revision);
#以下操作均不执行。因为target->init和target->extra_ops都为NULL if (target->init != NULL) target->init(target->t); opts = merge_options(opts, target->extra_opts, &target->option_offset); } |
至此,对用户的命令行输入的参数就算全部解析完成了,其中:
并完成了对struct ipt_entry{}中struct ipt_ip{}结构体成员的初始化,即对fw.ip的初始化。
(7)、参数和合法性检查
如果是“ADRI”操作但是没有指定源目的地址,默认将其置为全网段0.0.0.0/0。然后,设置源目的掩码fw.ip.smsk和fw.ip.dmsk。
检查command和options的匹配性generic_opt_check(command, options)。它们的相关性由一个二维数组commands_v_options[][]来限定:至此,所有的解析、校验工作都已完成。接下来我们将要探究,iptables如何与内核交互的问题。
未完,待续…
iptables用户空间和内核空间的交互
iptables目前已经支持IPv4和IPv6两个版本了,因此它在实现上也需要同时兼容这两个版本。iptables-1.4.0在这方面做了很好的设计,主要是由libiptc库来实现。libiptc是iptables control library的简称,是Netfilter的一个编程接口,通常被用来显示、操作(查询、修改、添加和删除)netfilter的规则和策略等。使用libipq库和ip_queue模块,几乎可以实现任何在内核中所实现的功能。
libiptc库位于iptables源码包里的libiptc目录下,共六个文件还是比较容易理解。我们都知道,运行在用户上下文环境中的代码是可以阻塞的,这样,便可以使用消息队列和 UNIX 域套接字来实现内核态与用户态的通信。但这些方法的数据传输效率较低,Linux 内核提供 copy_from_user()/copy_to_user() 函数来实现内核态与用户态数据的拷贝,但这两个函数会引发阻塞,所以不能用在硬、软中断中。一般将这两个特殊拷贝函数用在类似于系统调用一类的函数中,此类函数在使用中往往"穿梭"于内核态与用户态。此类方法的工作原理为:
其中相关的系统调用是需要用户自行编写并载入内核。一般情况都是,内核模块注册一组设置套接字选项的函数使得用户空间进程可以调用此组函数对内核态数据进行读写。我们的libiptc库正是基于这种方式实现了用户空间和内核空间数据的交换。
为了后面便于理解,这里我们简单了解一下在socket编程中经常要接触的两个函数:
int setsockopt(int sockfd, int proto, int cmd, void *data, int datalen)
int getsockopt(int sockfd, int proto, int cmd, void *data, int datalen)
这个两个函数用来控制相关socket文件描述符的一些选项值,如设置(获取)接受或发送缓冲区的大小、设置(获取)接受或发送超时值、允许(禁止)重用本地端口和地址等等。
参数说明:
sockfd:为socket的文件描述符;
proto:sock协议,IP RAW的就用SOL_SOCKET/SOL_IP等,TCP/UDP socket的可用SOL_SOCKET/SOL_IP/SOL_TCP/SOL_UDP等,即高层的socket是都可以使用低层socket的命令字 的;
cmd:操作命令字,由自己定义,一般用于扩充;
data:数据缓冲区起始位置指针,set操作时是将缓冲区数据写入内核,get的时候是将内核中的数据读入该缓冲区;
datalen:参数data中的数据长度。
我们可以通过扩充新的命令字(即前面的cmd字段)来实现特殊应用程序的内核与用户空间的数据交换,内核实现新的sockopt命令字有两类:一类是添加完整的新的协议后引入;一类是在原有协议命令集的基础上增加新的命令字。以netfilter为例,它就是在原有的基础上扩展命令字,实现了内核与用户空间的数据交换。Netfilter新定义的命令字如下:
setsockopt新增命令字:
#define IPT_SO_SET_REPLACE //设置规则
#define IPT_SO_SET_ADD_COUNTERS //加入计数器
getsockopt新增命令字;
#define IPT_SO_GET_INFO //获取ipt_info
#define IPT_SO_GET_ENTRIES //获取规则
#define IPT_SO_GET_REVISION_MATCH //获取match
#define IPT_SO_GET_REVISION_TARGET //获取target
一个标准的setsockopt()操作的调用流程如下:
在ip_setsockopt调用时,如果发现是一个没有定义的协议,并且判断现在这个optname是否为netfilter所设置,如果是则调用netfilter所设置的特殊处理函数,于是加入netfilter对sockopt特殊处理后,新的流程如下:
netfitler对于会实例化一些struct nf_sockopt_ops{}对象,然后通过nf_register_sockopt()将其注册到全局链表nf_sockopts里。
struct nf_sockopt_ops { struct list_head list; int pf;
/* Non-inclusive ranges: use 0/0/NULL to never get called. */ int set_optmin; int set_optmax; int (*set)(struct sock *sk, int optval, void __user *user, unsigned int len); int (*compat_set)(struct sock *sk, int optval,void __user *user, unsigned int len);
int get_optmin; int get_optmax; int (*get)(struct sock *sk, int optval, void __user *user, int *len); int (*compat_get)(struct sock *sk, int optval,void __user *user, int *len);
/* Number of users inside set() or get(). */ unsigned int use; struct task_struct *cleanup_task; }; |
继续回到libiptc中。libiptc库中的所有函数均以“iptc_”开头,主要有下面一些接口(节选自libiptc.h):
typedef struct iptc_handle *iptc_handle_t;
/* Does this chain exist? */ int iptc_is_chain(const char *chain, const iptc_handle_t handle);
/* Take a snapshot of the rules. Returns NULL on error. */ iptc_handle_t iptc_init(const char *tablename);
/* Cleanup after iptc_init(). */ void iptc_free(iptc_handle_t *h);
/* Iterator functions to run through the chains. Returns NULL at end. */ const char *iptc_first_chain(iptc_handle_t *handle); const char *iptc_next_chain(iptc_handle_t *handle);
/* Get first rule in the given chain: NULL for empty chain. */ const struct ipt_entry *iptc_first_rule(const char *chain,iptc_handle_t *handle); /* Returns NULL when rules run out. */ const struct ipt_entry *iptc_next_rule(const struct ipt_entry *prev,iptc_handle_t *handle);
/* Returns a pointer to the target name of this entry. */ const char *iptc_get_target(const struct ipt_entry *e,iptc_handle_t *handle);
/* Is this a built-in chain? */ int iptc_builtin(const char *chain, const iptc_handle_t handle);
int iptc_append_entry(const ipt_chainlabel chain, const struct ipt_entry *e, iptc_handle_t *handle);
/* Zeroes the counters in a chain. */ int iptc_zero_entries(const ipt_chainlabel chain,iptc_handle_t *handle);
/* Creates a new chain. */ int iptc_create_chain(const ipt_chainlabel chain,iptc_handle_t *handle);
/* Makes the actual changes. */ int iptc_commit(iptc_handle_t *handle);
|
上面这些接口都是为IPv4定义了,同样的IPv6的接口均定义在libip6tc.h头文件中,都以“ip6tc_”开头(如此说来,IPv4的头文件应该叫libip4tc.h才比较合适)。然后在libip4tc.c和libip6tc.c文件中分别通过宏定义的形式将IPv4和IPv6对外的接口均统一成“TC_”开头的宏,并在libiptc.c中实现这些宏即可。如下图所示:
这里我们看到iptables-v4和iptables-v6都和谐地统一到了libiptc.c中,后面我们分析的时候只要分析这些相关的宏定义的实现即可。
在继续往下分析之前我们先看一下STRUCT_TC_HANDLE这个比较拉风的结构体,它用于存储我们和内核所需要交换的数据。说的通俗一些,就是从内核中取出的表的信息会存储到该结构体类型的变量中;当我们向内核提交iptables变更时,也需要一个该结构体类型的变量用于存储我们所要提交的数据。(定义在ip_tables.h头文件中)
适用于当getsockopt的参数为IPT_SO_GET_INFO,用于从内核读取表信息 struct ipt_getinfo #define STRUCT_GETINFO struct ipt_getinfo { /* Which table: caller fills this in. */ #从内核取出的表信息会存储在该结构体中 char name[IPT_TABLE_MAXNAMELEN]; /* Kernel fills these in. */ unsigned int valid_hooks; /* Which hook entry points are valid: bitmask */ unsigned int hook_entry[NF_IP_NUMHOOKS]; // Hook entry points: one per netfilter hook. unsigned int underflow[NF_IP_NUMHOOKS]; /* Underflow points. */ unsigned int num_entries; /* Number of entries */ unsigned int size; /* Size of entries. */ }; |
还有一个成员entries用保存表中的所有规则信息,每条规则都是一个ipt_entry的实例:
/* The argument to IPT_SO_GET_ENTRIES. */ struct ipt_get_entries { /* Which table: user fills this in. */ char name[IPT_TABLE_MAXNAMELEN]; unsigned int size; /* User fills this in: total entry size. */
struct ipt_entry entrytable[0]; /*内核里表示规则的结构,参见博文三. */ }; |
(一)、从内核获取数据:iptc_init()
都说“磨刀不误砍柴工”,接下来我们继续上一篇中do_command()函数里剩下的部分。*handle = iptc_init(*table); 这里即根据表名table去从内核中获取该表的自身信息和表中的所有规则。关于表自身的一些信息存储在handle->info成员里;表中所有规则的信息保存在handle->entries成员里。
如果handle获取失败,则尝试加载完内核中相应的ko模块后再次执行iptc_init()函数。
然后,针对“ADRI”操作需要做一些合法性检查,诸如-o选项不能用在PREROUTING和INPUT链中、-i选项不能用在POSTROUTING和OUTPUT链中。
if (target && iptc_is_chain(jumpto, *handle)) { fprintf(stderr,"Warning: using chain %s, not extension\n",jumpto); if (target->t) free(target->t);
printf("Target is a chain,but we have gotten a target,then free it!\n");
target = NULL; } |
如果-j XXX 后面的XXX是一条用户自定义规则链,但是之前却解析出了标准target,那么需要将target的空间释放掉。很明显,目前我们的-j ACCEPT不会执行到这里。
if (!target #如果没有指定target。同样,我们的规则也不会执行到这里 && (strlen(jumpto) == 0|| iptc_is_chain(jumpto, *handle)) #或者target是一条链或为空 ) { size_t size; … … } |
因为我们的target为ACCEPT,已经被完全正确解析,即target!=NULL。后面我们会执行else条件分子如下的代码:
e = generate_entry(&fw, matches, target->t);
用于生成一条iptables的规则,它首先会为e去申请一块大小n*match+target的空间,其中n为用户输入的命令行中的match个数,target为最后的动作。这里很明显,我们的命令只有一个tcp的match,target是标准target,即ACCEPT。将已经解析的fw赋给e,并对结构体e中其他的成员进行初始化,然后将相应的match和target的数据拷贝到e中对应的成员中。
size = sizeof(struct ipt_entry); for (matchp = matches; matchp; matchp = matchp->next) size += matchp->match->m->u.match_size;
e = fw_malloc(size + target->u.target_size); *e = *fw; e->target_offset = size; e->next_offset = size + target->u.target_size;
size = 0; for (matchp = matches; matchp; matchp = matchp->next) { memcpy(e->elems + size, matchp->match->m, matchp->match->m->u.match_size); size += matchp->match->m->u.match_size; } memcpy(e->elems + size, target, target->u.target_size); |
最后所生成的规则e,其内存结构如下图所示:
这里再联系我们对内核中netfilter的分析就很容易理解了,一旦我们获取一条规则ipt_entry的首地址,那么我们能通过target_offset很快获得这条规则的target地址,同时也可以通过next_offset获得下一条ipt_entry规则的起始地址,很方便我们到时候做数据包匹配的操作。
紧接着就是对解析出来的command命令进行具体操作,这里我们是-A命令,因此最后command命令就是CMD_APPEND,这里则执行append_entry()函数。
ret = append_entry(chain, #链名,这里为INPUT
e, #将用户的命令解析出来的最终的规则对象
nsaddrs, #-s 后面源地址的个数
saddrs, #用于保存源地址的数组
ndaddrs, #-d 后面的目的地址的个数
daddrs, #用于保存目的地址的数组
options&OPT_VERBOSE, #iptables命令是否有-v参数
handle #从内核中取出来的规则表信息
);
在append_entry内部调用了iptc_append_entry(chain, fw, handle),其实就是由宏即TC_APPEND_ENTRY所表示的那个函数。该函数内部有两个值得注意的结构体类型struct chain_head{}和struct rule_head{},分别用于保存我们所要操作的链以及链中的规则:
struct chain_head { struct list_head list; char name[TABLE_MAXNAMELEN]; unsigned int hooknum; /* hook number+1 if builtin */ unsigned int references; /* 有多少-j 指定了我们的名字 */ int verdict; /* verdict if builtin */ STRUCT_COUNTERS counters; /* per-chain counters */ struct counter_map counter_map; unsigned int num_rules; /* 本链中的规则数*/ struct list_head rules; /* 本链中所有规则的入口点 */
unsigned int index; /* index (needed for jump resolval) */ unsigned int head_offset; /* offset in rule blob */ unsigned int foot_index; /* index (needed for counter_map) */ unsigned int foot_offset; /* offset in rule blob */ };
struct rule_head { struct list_head list; struct chain_head *chain; struct counter_map counter_map; unsigned int index; /* index (needed for counter_map) */ unsigned int offset; /* offset in rule blob */
enum iptcc_rule_type type; struct chain_head *jump; /* jump target, if IPTCC_R_JUMP */
unsigned int size; /* size of entry data */ STRUCT_ENTRY entry[0]; #真正的规则入口点 sizeof计算时不会包含这个字段 }; |
TC_APPEND_ENTRY的函数实现:
int TC_APPEND_ENTRY(const IPT_CHAINLABEL chain, const STRUCT_ENTRY *e, TC_HANDLE_T *handle) #注意:这里的handle是个二级指针 { struct chain_head *c; struct rule_head *r;
iptc_fn = TC_APPEND_ENTRY; if (!(c = iptcc_find_label(chain, *handle))) { #根据链名查找真正的链地址赋给c,此时c就指向了INPUT链的内存, #包括INPUT中的所有规则和它的policy等 DEBUGP("unable to find chain `%s'\n", chain); errno = ENOENT; return 0; }
if (!(r = iptcc_alloc_rule(c, e->next_offset))) { #ipt_entry的next_offset即指明了下一条规则的起始地址,同时这个值也说明了本条规则所占了存储空间的大小。这里所申请的空间大小=sizeof(rule_head)+当前规则所占的空间大小。 DEBUGP("unable to allocate rule for chain `%s'\n", chain); errno = ENOMEM; return 0; }
memcpy(r->entry, e, e->next_offset); #把规则拷贝到柔性数组entry中去 r->counter_map.maptype = COUNTER_MAP_SET;
if (!iptcc_map_target(*handle, r)) { #主要是设置规则r的target,后面分析。 DEBUGP("unable to map target of rule for chain `%s'\n", chain); free(r); return 0; }
list_add_tail(&r->list, &c->rules); #将新规则r添加在链c的末尾 c->num_rules++; #同时将链中的规则计数增加
set_changed(*handle); #因为INPUT链中的规则已经被改变,则handle->changed=1; return 1; } |
接下来分析一下设置target时其函数内部流程:
static int iptcc_map_target(const TC_HANDLE_T handle, struct rule_head *r) { STRUCT_ENTRY *e = r->entry; #取规则的起始地址 STRUCT_ENTRY_TARGET *t = GET_TARGET(e); #取规则的target
/* Maybe it's empty (=> fall through) */ if (strcmp(t->u.user.name, "") == 0) { #如果没有指定target,则将规则类型设为“全放行” r->type = IPTCC_R_FALLTHROUGH; return 1; }
/* Maybe it's a standard target name... */ #因为都是标准target,因此将target中用户空间的user.name都置为空,设置verdict, #并将rule_head中的type字段为IPTCC_R_STANDARD else if (strcmp(t->u.user.name, LABEL_ACCEPT) == 0) return iptcc_standard_map(r, -NF_ACCEPT - 1); else if (strcmp(t->u.user.name, LABEL_DROP) == 0) return iptcc_standard_map(r, -NF_DROP - 1); else if (strcmp(t->u.user.name, LABEL_QUEUE) == 0) return iptcc_standard_map(r, -NF_QUEUE - 1); else if (strcmp(t->u.user.name, LABEL_RETURN) == 0) return iptcc_standard_map(r, RETURN); else if (TC_BUILTIN(t->u.user.name, handle)) { /* Can't jump to builtins. */ errno = EINVAL; return 0; } else { /* 如果跳转的目标是一条用户自定义链,则执行下列操作*/ struct chain_head *c; DEBUGP("trying to find chain `%s': ", t->u.user.name); c = iptcc_find_label(t->u.user.name, handle); #找到要跳转的目的链的入口地址 if (c) { DEBUGP_C("found!\n"); r->type = IPTCC_R_JUMP; #将rule_head结构的type字段置为“跳转” r->jump = c; #跳转的目标为t->u.user.name所指示的链 c->references++; #跳转到的目的链因此而被引用了一次,则计数器++ return 1; } DEBUGP_C("not found :(\n"); }
/* 如果不是用户自定义链,它一定一个用户自定义开发的target模块,比如SNAT、LOG等。If not, kernel will reject... */ /* memset to all 0 for your memcmp convenience: don't clear version */ memset(t->u.user.name + strlen(t->u.user.name), 0, FUNCTION_MAXNAMELEN - 1 - strlen(t->u.user.name)); r->type = IPTCC_R_MODULE; #比如SNAT,LOG等会执行到这里 set_changed(handle); return 1; } |
在append_entry()函数最后,将执行的执行结果返回给ret,1表示成功;0表示失败。然后在做一下善后清理工作,如果命令行中有-v则将内核中表的快照dump一份详细信息出来显示给用户看:
if (verbose > 1)
dump_entries(*handle);
clear_rule_matches(&matches); //释放matches所占的存储空间
由struct ipt_entry e;所存储的规则信息已经被提交给了handle对象对应的成员,因此将e所占的存储空间也释放:
if (e != NULL) {
free(e);
e = NULL;
}
将全局变量opts复位,初始化时opts=original_opts。因为在解析--syn时tcp的解析参数被加进来了:
static struct option original_opts[] = {
{ "append", 1, NULL, 'A' },
{ "delete", 1, NULL, 'D' },
… …
}
至此,do_command()函数的执行就算全部完成了。
(二)、向内核提交变更:iptc_commit()
执行完do_command()解析完命令行参数后,用户所作的变更仅被提交给了handle这个结构体变量,这个变量里的所有数据在执行iptc_commit()函数前都驻留在内存里。因此,在iptables-standalone.c里有如下的代码语句:
ret = do_command(argc, argv, &table, &handle); if (ret) ret = iptc_commit(&handle); |
当do_command()执行成功后才会去执行iptc_commit()函数,将handle里的数据提交给Netfilter内核。
iptc_commit()的实现函数为int TC_COMMIT(TC_HANDLE_T *handle),我们只分析IPv4的情形,因此专注于libiptc.c文件中该函数的实现。
在TC_COMMIT()函数中,又出现了我们在分析Netfilter中filter表时所见到的一些重要结构体STRUCT_REPLACE *repl;STRUCT_COUNTERS_INFO *newcounters;还有前面出现的struct chain_head *c;结构体。
new_number = iptcc_compile_table_prep(*handle, &new_size);
iptcc_compile_table_prep()该函数主要做的工作包含几个方面:
a.初始化handle里每个struct chain_head{}结构体成员中的head_offset、foot_index和foot_offset。
b.对每个链(struct chain_head{})中的每条规则,再分别计算它们的offset和index。
c.计算handle所指示的表中所有规则所占的存储空间的大小new_size,以及规则的总条数new_number。
接下来,为指针repl;申请存储空间,所申请的大小为sizeof(struct ipt_replace)+new_size。因为struct ipt_replace{}结构的末尾有一个柔性数组struct ipt_entry entries[0]; 它是不计入sizeof的计算结果的。因此,iptables的所有规则实际上是存储在struct ipt_entry entries[0]柔性数组中的,这里所有规则所占大小已经得到:new_size。
因为,每条规则entry都一个计数器,用来记录该规则处理了多少数据包,注意结构体STRUCT_COUNTERS_INFO{}的末尾也有一个柔性数组struct xt_counters counters[0];其中struct xt_counters{}才是真正的用于统计数据包的计数器。
然后开始初始化repl结构:
strcpy(repl->name, (*handle)->info.name); repl->num_entries = new_number; repl->size = new_size;
repl->num_counters = (*handle)->info.num_entries; repl->valid_hooks = (*handle)->info.valid_hooks; |
紧接着对repl结构体中剩下的成员进行初始化,hook_entry[]、underflow[]等。对于用户自定义链,其末尾的target.verdict=RETURN。
setsockopt(sockfd, TC_IPPROTO, SO_SET_REPLACE, repl,sizeof(*repl) + repl->size);
会触发内核去执行前面我们看到的do_ipt_set_ctl()函数,如下:
static struct nf_sockopt_ops ipt_sockopts = { .pf = PF_INET, .set_optmin = IPT_BASE_CTL, .set_optmax = IPT_SO_SET_MAX+1, .set = do_ipt_set_ctl, .get_optmin = IPT_BASE_CTL, .get_optmax = IPT_SO_GET_MAX+1, .get = do_ipt_get_ctl, }; |
在do_ipt_set_ctl()中其核心还是执行do_replace()函数:
static int do_replace(void __user *user, unsigned int len) { int ret; struct ipt_replace tmp; struct xt_table_info *newinfo; void *loc_cpu_entry;
if (copy_from_user(&tmp, user, sizeof(tmp)) != 0) return -EFAULT;
/* Hack: Causes ipchains to give correct error msg --RR */ if (len != sizeof(tmp) + tmp.size) return -ENOPROTOOPT; … … } |
其中copy_from_user()负责将用户空间的repl变量中的内容拷贝到内核中的tmp中去。然后设置规则计数器newcounters,通过setsockopt系统调用将newcounters设置到内核:
setsockopt(sockfd, TC_IPPROTO, SO_SET_ADD_COUNTERS, newcounters, counterlen);
此时,在do_ipt_set_ctl()中执行的是do_add_counters()函数。至此,iptables用户空间的所有代码流程就算分析完了。命令:
iptables –A INPUT –i eth0 –p tcp --syn –s 10.0.0.0/8 –d 10.1.28.184 –j ACCEPT
即被设置到内核的Netfilter规则中去了。
未完,待续…
转载地址:http://mnhvi.baihongyu.com/