最近在看iptables的源码,得益于九贱兄的《iptables源码分析》和几位朋友的指点,终于完成了iptables代码的了解。不过,我分析代码的方式,主要是从一条命令的执行上来分析整个iptables的执行流程。感觉这样分析起来更加有条理,容易把握住iptables的整个代码。
我是边分析边总结,所以代码分析完了,总结也就写完了。不过,我这里并不注重对具体代码的分析,而是注重代码的指令流程以及结果。希望可以对准备学习iptables的朋友有所帮助。
分析的版本是1.2.9。分析难免有错误和不妥的地方,欢迎指正,多多交流,共同进步。欢迎转载,当请保持该文档的完整性,并请注明出处。 CU: Godbach Blog:http://Godbach. 2008/08/12
这里以一条iptabls的命令来分析Iptables的代码,该条规则尽量设计比较多的iptables的命令行参数。 假设要下的规则为: iptabls –A INPUT –i eth0 –p tcp --syn –s 10.0.0.0/8 –d 10.1.28.184 –j ACCEPT 该规则是将源地址段在10.0.0.0/8范围之内的主机发送的SYN包,接受的网口为eth0,且目的地址为10.1.28.184的,执行ACCEPT
以下是执行该条命令时,iptables的流程分析。这里比较不注重分析代码,而注重执行的流程及结果。
一、命令行的入口
int main(int argc, char *argv[]) (iptables-standalone.c) 整个iptables命令行的入口。该函数设置了默认的table=”filter”,即当命令行中没有-t选项是,就使用默认的filter表。同时还初始化了该应用程序的名字,版本号等。 #ifdef NO_SHARED_LIBS init_extensions(); #endif 以上代码为没有定义共享库的话,要执行init_extensions()。这里我们假设不使用共享库,所以调用该函数。该函数实在执行make的时候extensions/自动生成的initext.c中的函数。在该函数里调用了所有扩展模块的init函数。 注册所有的match,以及标准和扩展的target。 所有的match和target都加入到iptables.c中对应的全局链表之中。以后find_match和find_target是就是搜索的这两个链表。 /* Keeping track of external matches and targets: linked lists. */ struct iptables_match *iptables_matches = NULL; struct iptables_target *iptables_targets = NULL; 这样在不使用共享库的情况下,每次下命令之前都要初始化一下全局的链表,当然已经存在的话,就不会再次register的。
二、命令行的核心处理
do_command()是处理Iptables命令的核心部分。 int do_command(int argc, char *argv[], char **table, iptc_handle_t *handle) (iptables.c)
1. 函数首先对一些结构、变量进行初始化。基本上涵盖了一条规则可能出现的大部分基本参数。其中,重要的结构体有struct ipt_entry fw, *e,这两个应该是存储一条防火墙规则的;struct iptables_match *m,struct iptables_target *target,*t; 分别用于存储match和target. 并将全局的match和target链表的对应flags和used为初始化为0.
2. 命令行解析。初始化完毕后,进入while循环,分析用户输入的命令,设置相关的标志变量,然后根据相应标志,调用对应的处理函数。这里是我们要进行详细分析的地方。 我们要分析的命令为: iptabls –A INPUT –i eth0 –p tcp --syn –s 10.0.0.0/8 –d 10.1.28.184 –j ACCEPT 以下开始命令行解析:
(1) 处理 –A 选项 调用add_command函数,主要是对command变量进行逻辑处理。该函数的输入参数 newcmd = CMD_APPEND=0x0010,othercmds= CMD_NONE=0x0000,invert = 0; 输出参数command(初始值为0), 执行完该函数之后, command = CMD_APPEND=0x0010, chain = “INPUT” 然后程序break跳出switch语句,并将invert = FALSE,进行while的下一个循环,及处理下一个选项。
(2) 处理 –i 选项 调用check_inverse函数。该函数主要是检查-i对应的参数中是否使用了取反标志”!”,因此这里我们使用的是”eth0”,因此该函数直接返回FALSE。invert=0。 调用set_option函数是指对应的选项。该函数的输入参数option= OPT_VIANAMEOUT(0x0080), invert=0,输出参数options(初始化为0),fw.ip.invflags(初始化为0)。 经过该函数处理之后, options = OPT_VIANAMEIN=0x0080; fw.ip.invflags=0. 调用parse_interface函数进行网络接口的解析。该函数的参数输入参数argv[optind-1]=”eth0”, 输出参数fw.ip.iniface[IFNAMSIZ]={“”}, fw.ip.iniface_mask[IFNAMSIZ]={“”}, 其中IFNAMSIZ=15。 处理之后, fw.ip.iniface[IFNAMSIZ]={“eth0”}; fw.ip.iniface_mask[IFNAMSIZ]={0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; fw.nfcache = NFC_IP_IF_IN = 0x0004;
(3) 处理 –p 选项 调用check_inverse函数,检查这里设置的协议参数是否使用了取反标志”!”。我们这里使用的是tcp,因此该函数直接返回FALSE。invert仍旧为0。 调用set_option函数是指对应的选项。可以参照(2)中该函数的处理方法。options 的初始值为0x0080,最终处理完毕之后 options |= OPT_PROTOCOL(0x0008) = 0x0088; fw.ip.invflags=0 然后协议字串tcp全部转化为小写,protocol = “tcp”。调用parse_protocol函数将该协议字符串转换为TCP对应的协议号6, fw.ip.proto = 6; fw.nfcache |= NFC_IP_PROTO = 0x0004 | 0x0020 = 0x0024;
(4) 处理 --syn选项 由于该选项并不在全局变量opts里面,所以程序会进入switch语句的default分支执行。 进行该分支处理时一些变量的初始值: target==NULL, m==NULL, 全局链表iptables_matches的used选项都为0 protocol = “tcp” ,options = 0x0088, proto_used = 0 根据以上参数的值,这里要执行的动作为加载该协议(m->init(), 即libipt_tcp.c中init()),并且将协议的一些选项扩展到全局变量opts里面。具体代码执行是进入该分支的if(m==NULL& amp;&…)里面,具体执行的结果如下: m指向全局match链表中tcp的match。(libipt_tcp.c) proto_used = 1; size = IPT_ALIGN(sizeof(struct ipt_entry_match)) + m->size; m->m = fw_calloc(1, size); (m->m为内核总的match部分) m->m->u.match_size = size; m->m->u.user.name = “tcp”; m->used = 1; 调用m->init(m->m, &fw.nfcache),执行init()(libipt_tcp.c)。仅是将match中的tcp的sport和dport的最大值置为 0xFFFF. struct ipt_entry_match 中的data主要用于表示真正数据部分的开始,是通过fw_calloc为整个结构体以及data申请的内存,以后对数据的操作都可以通过data来索引。 init(struct ipt_entry_match *m, unsigned int *nfcache) { struct ipt_tcp *tcpinfo = (struct ipt_tcp *)m->data; tcpinfo->spts[1] = tcpinfo->dpts[1] = 0xFFFF; } merge_options将tcp的一些选项扩展到全局变量opts里面。这样就可以解析 --syn选项了。opts包含了tcp协议的选项参数。 并且全局变量global_option_offset += OPTION_OFFSET = 256; m->option_offset= global_option_offset =256; 这里有点需要注意,就是新加到opts全局结构体中的部分,即tcp对应的option结构中所有的val成员都被赋值为: merge[num_old + i].val += *option_offset; (见merge_options函数) 这里主要是为了区别和原先已有option中的val成员。而且在以后添加更多match模块的时候,都要做这样的动作。因此在命令行解析的时候,通过 getopt_long得到的这些match中的命令行参数值的时候,需要先减去对应的m->option_offset,然后才能正确的 parse. 然后两行代码: optind--; continue; 是让程序进入下一个while循环,并再次处理--syn参数。因为以上的处理只是将tcp协议的match进行了初始化工作,并没有处理该参数。 因此,程序再次进入switch的default分支,不过这次是在if(!target&&…),由于对应tcp的match结构体的 used被置1,即m->used =1; 因此程序要进行m->parse进行libipt_tcp中的parse()函数对命令行参数进行处理。该函数的第一个参数的使用方法上面已经解释过了。以下就是parse()函数对--syn的处理结果: ((struct ipt_tcp *)m->data)->flg_mask = 0x16(SYN,ACK,RST); ((struct ipt_tcp *)m->data)->flg_cmp = 0x02(SYN) m->flags |= TCP_FLAGS = 0x04; fw.nfcache |= NF_IP_TCP_FLAGS(0x0100) = 0x0024 | 0x0100=0x0124 至此,iptables对—syn选项的解析已经完成。
(5) 处理-s选项 同样还是check_inverse,set_option的处理,只有options变量改变; options |= OPT_SOURCE(0x0002) = 0x0088 | 0x0002 = 0x008a; shostnetworkmask= “10.0.0.0/24”; fw.nfcache |= NFC_IP_SRC(0x0001)= 0x0124 | 0x0001 = 0x0125;
(6) 处理-d选项 同样还是check_inverse,set_option的处理,只有options变量改变; options |= OPT_DESTINATION (0x0004) = 0x008a | 0x0004 = 0x008e; dhostnetworkmask= “10.1.28.184”; fw.nfcache |= NFC_IP_DST (0x0002)= 0x0125 | 0x0002 = 0x0127;
(7) 处理-j选项 首先还是set_option函数的处理: options |= OPT_JUMP(0x0010) = 0x008e | 0x0010 = 0x009e; 然后jumpto =parse_target(“ACCEPT”),主要是检查一下该字符串是否合法; 接着target =find_target(jumpto, TRY_LOAD);该函数返回target为”standard”(包含了ACCEPT, DROP, QUEUE,RETURN等目标)的结构体指针。 并且: target->loaded = 1; target->used = 1 jumpto = “ACCEPT” size = IPT_ALIGN(sizeof(struct ipt_entry_target))+ target->size; target->t = fw_calloc(1, size); //分配内存给struct ipt_entry_target target->t->u.target_size = size; target->t->u.user.name = “ACCEPT” 这里的target->init指向了libipt_standard.c中的init(),该函数并未执行任何动作。 merge_options又是将该libipt_standard.c的 opts加入到全局的opts中。由于ACCEPT目标是标准的,这里实际上并未往全局的opts中添加任何内容。 仅修改了如下变量 global_option_offset += OPTION_OFFSET = 256+256=512; target->option_offset= global_option_offset =512; 至此,命令行已经解析完毕,下面要接着对解析出来的各个参数进行进一步处理。
3. 相关参数的检查
/*执行libipt_tcp.c中的final_check*/ m->final_check(m->mflags); /*执行libipt_standard.c中的final_check*/ target->final_check(target->tflags); 随后是对optind,invert,command的检查,接着的if (command &…)对我们的参数没有产生任何影响。 分别对shostnetworkmask,dhostnetworkmask调用parse_hostnetworkmask函数,执行的结果如下: saddrs[0]->s_addr = 0x0a000000 (10.0.0.0); fw.ip.smsk.s_addr = 0xff000000 (255.0.0.0); nsaddrs = 1; daddrs[0]->s_addr = 0x0a011cb8 (10.1.28.184); fw.ip.dmsk.s_addr = 0xffffffff (255.255.255.255); ndaddrs = 1; 最后调用generic_opt_check(command, options)对command和options进行检查。主要是检查iptables中的command和options是否搭配。所有的搭配情况保存在全局的数组 static char commands_v_options[NUMBER_OF_CMD][NUMBER_OF_OPT]. 下面则要从内核表里取出对应表的全部信息。
4. 取出内核中相应表的全部信息 因为在main函数中iptc_handle_t handle = NULL; 所以这里要执行的代码为: *handle = iptc_init(*table) 这里返回一个iptc_handle_t *handle的结构体指针,该指针指向从内核中去中的”filter”(默认)表对应的所有信息。 先取出info结构,获取表的一些大概信息,然后再获取整个表的规则。 随后的两个if (!*handle)都是判断*handle是否为NULL,如果是的话则说明有错误,获取不到对应表的相关信息。我们这里应该指向了获取的”filter”的信息的指针。因此这两个if判断都为FALSE。
5. 检查chain与相关options的搭配,target的合法等问题 整个检查都在if (command == CMD_APPEND…){…}里面进行。 首先检查chain和options的匹配情况,这里chain=”INPUT”,对于本例中设置的规则,应该都没有问题。 然后是if (target && iptc_is_chain(jumpto, *handle))。其中target为”standard”(包含了ACCEPT, DROP, QUEUE,RETURN等目标)的结构体指针。 (libiptc.c)。由于我们这里的jumpto为目标ACCETP,而并非chain,所以该if为FALSE. 因此,程序真正执行的部分是 e = generate_entry(&fw, iptables_matches, target->t); 用来生成一个struct ipt_entry e。e中包括了struct ipt_entry,并利用最后一个元素unsigned char elems[0]申请了一块内存,该内存里首先是若干个match的结构体,然后是一个target结构体,因此该块内存的大小为n*match + target.
6. 具体命令的执行 这部分也是do_command函数的最后。用一个swicth(command)来判断具体执行什么动作。我们这里 command=CMD_APPEND,因此调用append_entry函数来将该条iptables规则加入进去。大致介绍一下 append_entry函数的功能: 源码中具体对应的函数名为TC_APPEND_ENTRY()。该函数首先调用find_label找到整个规则中指定chain的struct chain_cache结构,然后做一下target的映射,我们这里是标准的target。真正执行添加规则的是insert_rules函数。该函数找到插入规则的entry点,并将该规则插入。 调整后的所有的规则都保存在结构体指针handle之中。
三、iptables规则的提交
do_command函数执行完毕,接着调用iptc_commit(&handle)将filter调整后的所有规则提交给内核。 这里只分析了ipt_init(),append_entry和iptc_commit的功能,没有具体分析其源代码。因为这三个函数的源代码都比较复杂。这里作为Iptables的流程分析,不具体讨论。 整个iptables的工作流程解析完毕。这样,对iptables的整个命令行的核心代码从已经有了相当的掌握。
|