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规则中去了。
未完,待续…