最近在看iptables的源码,得益于九贱兄的《iptables源码分析》和几位朋友的指点,终于完成了iptables代码的了解。不过,我分析代码的方式,主要是从一条命令的执行上来分析整个iptables的执行流程。感觉这样分析起来更加有条理,容易把握住iptables的整个代码。
我是边分析边总结,所以代码分析完了,总结也就写完了。不过,我这里并不注重对具体代码的分析,而是注重代码的指令流程以及结果。希望可以对准备学习iptables的朋友有所帮助。
分析的版本是1.2.9。整个总结从2楼开始。
这里以一条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&&…)里面,具体执行的结果如下:
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的整个命令行的核心代码从已经有了相当的掌握。
转自:
阅读(1317) | 评论(0) | 转发(0) |