- 论坛徽章:
- 0
|
本帖最后由 狼族狼心 于 2010-07-21 11:01 编辑
4.4 iptables的实现机制
这里分析的iptables版本是1.3.6,iptables作为用户态一个进程,入口函数比较简单,非常清晰明了,添加注释如下:
这里可以看到其中的do_command是处理的核心函数;
4.4.1 命令格式
如允许所有包通过:
#iptables –p input accept
具体的命令格式不在累赘,可网上查询;
4.4.2 命令解析
命令行敲入命令后需要解析,解析在函数do_command中进行;
具体源码分析如下:(蓝色为代码标注)
(该分析引自:http://linux.chinaunix.net/bbs/viewthread.php?tid=663849)
do_command 函数是整个系统的核心,负责处理整个用户的输入命令。函数首先对一些结构、变量进行初始化,初始化完毕后,进入while循环,分析用户输入的命令,设置相关的标志变量,然后根据相应标志,调用对应的处理函数。
struct ipt_entry fw, *e = NULL;
int invert = 0;
unsigned int nsaddrs = 0, ndaddrs = 0;
struct in_addr *saddrs = NULL, *daddrs = NULL;
int c, verbose = 0;
const char *chain = NULL;
const char *shostnetworkmask = NULL, *dhostnetworkmask = NULL;
const char *policy = NULL, *newname = NULL;
unsigned int rulenum = 0, options = 0, command = 0;
const char *pcnt = NULL, *bcnt = NULL;
int ret = 1;
struct iptables_match *m;
struct iptables_target *target = NULL;
struct iptables_target *t;
const char *jumpto = "";
char *protocol = NULL;
const char *modprobe = NULL;
/*初始化变量*/
memset(&fw, 0, sizeof(fw));
opts = original_opts;
global_option_offset = 0;
/* re-set optind to 0 in case do_command gets called
* a second time */
optind = 0;
/*初始化两个全局变量*/
/* clear mflags in case do_command gets called a second time
* (we clear the global list of all matches for security)*/
for (m = iptables_matches; m; m = m->next) {
m->mflags = 0;
m->used = 0;
}
for (t = iptables_targets; t; t = t->next) {
t->tflags = 0;
t->used = 0;
}
ps:开头一大堆的变量定义和初始化,可以在程序分析的时候看它们的作用,有两个全局结构变量很重要:iptables_matches和iptables_targets。现在来分析他们的作用会有一点困难,因为它们涉及到了太多方面的东东,这里,可以先把它们“想像成”用户空间用来读取内核规则的结构(当然,这有点错误)。
/*开始化析命令行*/
while ((c = getopt_long(argc, argv,
"-A:C :R:I:L::M:F::Z::N:X::E :Vh :p:s:d:j:i:fbvnt:m c:",
opts, NULL)) != -1)
{
}
这个while循环处理所有的用户输入,对应规则输出-L,有:
case 'L':
add_command(&command, CMD_LIST, CMD_ZERO,
invert);
if (optarg) chain = optarg;
else if (optind < argc && argv[optind][0] != '-'
&& argv[optind][0] != '!')
chain = argv[optind++];
break;
add_command函数负责将命令标志变量command与令标志 CMD_LIST求&运算, CMD_ZERO只是一个附加的判断标志而已,invert);然后,从命令行中取得要显示的链名(如果有的话)。
与此相关的还有用t参数指定了表名:
case 't':
if (invert)
exit_error(PARAMETER_PROBLEM,
"unexpected ! flag before --table" ;
*table = argv[optind-1];
break;
即,如果有’t’参数,则取’t’后跟的表名:*table = argv[optind-1],否则,它应该是主函数中默认的filter表。
命令处理完毕后,即进入执行模块:
/*因为程序定义了共享库的话,iptables_matches/iptables_target这两个结构运行至此是NULL,并且target也是NULL,对于规则显示而言,这一部份的处理目前没有实际意义,回过头再来看这一段更易理解。final_check成员函数的作用是作最终的标志检查,如果检测失则,则退出*/
for (m = iptables_matches; m; m = m->next) {
if (!m->used)
continue;
m->final_check(m->mflags);
}
if (target)
target->final_check(target->tflags);
接着对参数作一些必要的合法性检查:
/* Fix me: must put inverse options checking here --MN */
if (optind < argc)
exit_error(PARAMETER_PROBLEM,
"unknown arguments found on commandline" ;
if (!command)
exit_error(PARAMETER_PROBLEM, "no command specified" ;
if (invert)
exit_error(PARAMETER_PROBLEM,
"nothing appropriate following !" ;
/*对于如果要进行(CMD_REPLACE | CMD_INSERT | CMD_DELETE | CMD_APPEND)处理来说,如果没有设置来源/目的地址及掩码,则给予它们一个默认值*/
if (command & (CMD_REPLACE | CMD_INSERT | CMD_DELETE | CMD_APPEND)) {
if (!(options & OPT_DESTINATION))
dhostnetworkmask = "0.0.0.0/0";
if (!(options & OPT_SOURCE))
shostnetworkmask = "0.0.0.0/0";
}
/*对来源/目的地址及掩码进行拆分,它们总是以 addr/mask的形式来出现的,根据’/’前面的字符串取得地址值,根据’/’后面的掩码位数,求得正确的掩码值,值得注意的是,同时要处理主机地址和网络地址的情况*/
if (shostnetworkmask)
parse_hostnetworkmask(shostnetworkmask, &saddrs,
&(fw.ip.smsk), &nsaddrs);
if (dhostnetworkmask)
parse_hostnetworkmask(dhostnetworkmask, &daddrs,
&(fw.ip.dmsk), &ndaddrs);
/*然后检查来源/目的网络地址的合法性*/
if ((nsaddrs > 1 || ndaddrs > 1) &&
(fw.ip.invflags & (IPT_INV_SRCIP | IPT_INV_DSTIP)))
exit_error(PARAMETER_PROBLEM, "! not allowed with multiple"
" source or destination IP addresses" ;
/*对命令行格式进行合法性检查*/
generic_opt_check(command, options);
以上代码可以看出,do_command函数完成对命令行的解析,并且做一些合法性检查;命令输入正确并解析完毕后,下一步是获取内核的所有规则;
4.4.3 获取内核规则表
源码解析如下:
(该分析引自:http://linux.chinaunix.net/bbs/viewthread.php?tid=663849)
do_command函数最后一个参数handle,是一个指向了具体表,如filter、nat表的句柄,这里判断,如果handle为空,则调用iptc_init,根据table的名称,让handle指针指向相应的表的地址空间,也就是把对应表的所有信息从内核中取出来:
/* only allocate handle if we weren't called with a handle */
if (!*handle)
*handle = iptc_init(*table);
/*如果获取换败,将试着插入模块,再次获取*/
if (!*handle) {
/* try to insmod the module if iptc_init failed */
iptables_insmod("ip_tables", modprobe);
*handle = iptc_init(*table);
/*仍然失败,则退出*/
if (!*handle)
exit_error(VERSION_PROBLEM,
"can't initialize iptables table `%s': %s",
*table, iptc_strerror(errno));
/*继续进行一些简单的判断*/
if (command == CMD_APPEND
|| command == CMD_DELETE
|| command == CMD_INSERT
|| command == CMD_REPLACE) {
/*List命令不在判断之列,暂时不分析*/
}
补充:
获取内核规则表的函数是iptc_init,该函数采用了转定义,具体的实现为TC_INIT,在文件libiptc.c中;下面具体对该函数进行分析:
(代码分析摘自:http://linux.chinaunix.net/bbs/viewthread.php?tid=663849 )
再回到iptc_init 函数上来,它根据表名,从内核获取对应的表的相关信息,handle是一个iptc_handle_t类型的指针,在libiptc.c中,有如下定义:
/* Transparent handle type. */
typedef struct iptc_handle *iptc_handle_t;
在Libip4tc中:
#define STRUCT_TC_HANDLE struct iptc_handle
在Libiptc.c中,可以找到STRUCT_TC_HANDLE的定义:
STRUCT_TC_HANDLE
{
/* Have changes been made? */
int changed;
/* Size in here reflects original state. */
STRUCT_GETINFO info;
struct counter_map *counter_map;
/* Array of hook names */
const char **hooknames;
/* Cached position of chain heads (NULL = no cache). */
unsigned int cache_num_chains;
unsigned int cache_num_builtins;
/* Rule iterator: terminal rule */
STRUCT_ENTRY *cache_rule_end;
/* Number in here reflects current state. */
unsigned int new_number;
STRUCT_GET_ENTRIES entries;
};
再来看看iptc_init函数,同样在在Libip4tc中,有如下定义:
#define TC_INIT iptc_init
在Libiptc.c中,可以看到函数的实现,基本上iptables与内核的交互,都是使用setsockopt函数来实现的,对于获取取规是信息来说,标志位是SO_GET_INFO,而从内核返回回来的规则信息是一个STRUCT_GETINFO结构:
TC_HANDLE_T TC_INIT(const char *tablename)
{
TC_HANDLE_T h;
STRUCT_GETINFO info;
unsigned int i;
int tmp;
socklen_t s;
iptc_fn = TC_INIT;
if (sockfd != -1)
close(sockfd);
/*为获取信息打开一个套接字接口*/
sockfd = socket(TC_AF, SOCK_RAW, IPPROTO_RAW);
if (sockfd < 0)
return NULL;
s = sizeof(info);
if (strlen(tablename) >= TABLE_MAXNAMELEN) {
errno = EINVAL;
return NULL;
}
strcpy(info.name, tablename);
/*获取规则信息*/
/*-- yangxh mark:第一次获取表的一些大概信息,info结构,此时并没有获取具体的规则--*/
if (getsockopt(sockfd, TC_IPPROTO, SO_GET_INFO, &info, &s) < 0)
return NULL;
/*-- yangxh mark:在第一次获取基础上知道了规则数以及总大小,此处申请空间存放规则,地址空间的地址保存在h->entries --*/
if ((h = alloc_handle(info.name, info.size, info.num_entries))
== NULL)
return NULL;
/* Too hard --RR */
#if 0
sprintf(pathname, "%s/%s", IPT_LIB_DIR, info.name);
dynlib = dlopen(pathname, RTLD_NOW);
if (!dynlib) {
errno = ENOENT;
return NULL;
}
h->hooknames = dlsym(dynlib, "hooknames" ;
if (!h->hooknames) {
errno = ENOENT;
return NULL;
}
#else
h->hooknames = hooknames;
#endif
/* Initialize current state */
h->info = info;
h->new_number = h->info.num_entries;
for (i = 0; i < h->info.num_entries; i++)
h->counter_map
= ((struct counter_map){COUNTER_MAP_NORMAL_MAP, i});
h->entries.size = h->info.size;
tmp = sizeof(STRUCT_GET_ENTRIES) + h->info.size;
/*--yangxh mark:此处才是真正获取具体的规则,规则内容存放在h->entries ,大小为tmp --*/
if (getsockopt(sockfd, TC_IPPROTO, SO_GET_ENTRIES, &h->entries,
&tmp) < 0) {
free(h);
return NULL;
}
CHECK(h);
return h;
}
函数为h分配空间,然后赋予相应的值。要理解这个函数,还需要了解STRUCT_GETINFO结构和分配内存空间的函数alloc_handle。
#define STRUCT_GETINFO struct ipt_getinfo
/* The argument to IPT_SO_GET_INFO */
struct ipt_getinfo
{
/* Which table: caller fills this in. */
char name[IPT_TABLE_MAXNAMELEN];
/* Kernel fills these in. */
/* Which hook entry points are valid: bitmask */
unsigned int valid_hooks;
/* Hook entry points: one per netfilter hook. */
unsigned int hook_entry[NF_IP_NUMHOOKS];
/* Underflow points. */
unsigned int underflow[NF_IP_NUMHOOKS];
/* Number of entries */
unsigned int num_entries;
/* Size of entries. */
unsigned int size;
};
/* Allocate handle of given size */
static TC_HANDLE_T
alloc_handle(const char *tablename, unsigned int size, unsigned int num_rules)
{
size_t len;
TC_HANDLE_T h;
len = sizeof(STRUCT_TC_HANDLE)
+ size
+ num_rules * sizeof(struct counter_map);
if ((h = malloc(len)) == NULL) {
errno = ENOMEM;
return NULL;
}
h->changed = 0;
h->cache_num_chains = 0;
h->cache_chain_heads = NULL;
h->counter_map = (void *)h
+ sizeof(STRUCT_TC_HANDLE)
+ size;
strcpy(h->info.name, tablename);
strcpy(h->entries.name, tablename);
return h;
}
补充:
1. 最终获取内核的规则是通过getsockopt函数实现的;
2. 调用了两次getsockopt函数,第一次是获取大概信息,主要是得到名字,规则数,以及占用空间大小,以便申请存储空间用;
3. 在iptables与内核的交互通过getsockopt实现,并通过指令(如SO_GET_INFO,该指令经过转定义,对于ip4来说iptables在libip4tc.c中转定义,内核对应的指令为IPT_SO_GET_INFO)来对应具体的操作,内核对应的处理函数为do_ipt_get_ctl和do_ipt_set_ctl,在ip_tables.c中;
4.4.4 按命令进行处理
(http://blog.chinaunix.net/u/33048/showart_1121090.html )
这部分也是do_command函数的最后。用一个swicth(command)来判断具体执行什么动作。我们这里 command=CMD_APPEND,因此调用append_entry函数来将该条iptables规则加入进去。大致介绍一下 append_entry函数的功能:
源码中具体对应的函数名为TC_APPEND_ENTRY()。该函数首先调用find_label找到整个规则中指定chain的struct chain_cache结构,然后做一下target的映射。真正执行添加规则的是insert_rules函数。该函数找到插入规则的entry点,并将该规则插入。
调整后的所有的规则都保存在结构体指针handle之中。
该逻辑结构比较简单,不在具体分析;
4.4.5 规则表提交到内核
整个do_command执行完后,也就完成了命令的解析,内核规则表的获取,具体动作的执行,下面将根据do_command处理结构决定是否需要将规则表提交到内核,如命令是现实所有规则,并不对规则进行修改,则不需要提交到内核;如果对规则进行了增删改,则需要提交;
提交规则到内核调用的函数是:iptc_commit;
iptc_commit的实现在libiptc.c中的TC_COMMIT;其中主要是通过setsockopt与内核交互,完成规则表的提交;
4.4.6 内核处理规则表
用户态最终通过setsockopt/ getsockopt与内核交互,内核对应的处理函数是do_ipt_set_ctl/ do_ipt_get_ctl;
do_ipt_get_ctl主要是获取规则,这里不讨论,主要看do_ipt_set_ctl,用户态提交规则表后内核态的处理过程;
函数do_ipt_set_ctl源码如下:
目前有两个分支,我们主要讨论IPT_SO_SET_REPLACE分支:将用户态提交的规则表,替换内核原有的规则表;即主要调用函数do_replace,该函数源码如下:
由以上源码可以看到,该函数的处理过程有四个:
1. 从用户空间将将新的规则表拷贝到内核空间;注意这里有两次拷贝(copy_from_user函数),第一次拷贝是大概信息,即ipt_replace结构,第二次才是拷贝真正的规则表信息;
2. 对新规则进行合法性检查(translate_table);
3. 根据规则表名字找到原来旧的规则表(find_table_lock);如果查不到则表示该规则是新建的,申请新的规则处理模块(try_then_request_module request_module);
4. 新的规则表替换旧的规则表(replace_table);
5. 对旧规则表进行清理释放(IPT_ENTRY_ITERATE, vfree);
6. 将结果返回给用户空间(copy_to_user);
**这其中有两个重点的函数需要关注:translate_table(见4..3.3),replace_table(见4..3.4) |
|