from:
http://linux.chinaunix.net/bbs/viewthread.php?tid=670248&extra=&page=1
前段时间 贴了一篇《iptables 源码 分析》,虽然后来没有写完,但是我发现有许多朋友都挺喜欢netfilter 的,于是我就有一个想法,大家能不能把各自学习的心得贴下来,写成一篇完整的文章呢?? 一来大家可以互相学习、交流、进步; 二来大家一起来完成,也希望能成为CU上一篇经典之作,体现CUer们的互相协作精神!!! 但是关键是要有条理,不要搞乱了,没了体系,就不太好了,对于这一点,还希望版主们如果支持我的意见,多费点神…… 我先来抛砖,把自己的开头部份的笔记稍改了一下,贴出来,开个头 欢迎大家指正(因为手头没有什么资料,都是一句句用sourceinsight跟的,错误难免了) 希望有志同道合的朋友继续写下去,期盼中……
一、主函数 init为初始化函数,主要完成表的注册,然后再注册与表相对应的HOOK //初始化函数为init: module_init(init); //init 函数负责注册filter表和默认的三个chain static int __init init(void) { int ret; if (forward < 0 || forward > NF_MAX_VERDICT) { printk("iptables forward must be 0 or 1/n"); return -EINVAL; } /* Entry 1 is the FORWARD hook */ initial_table.entries[1].target.verdict = -forward - 1; /* 注册filter表 */ ret = ipt_register_table(&packet_filter); if (ret < 0) return ret; /* 注册各个钩子函数 */ ret = nf_register_hook(&ipt_ops[0]); if (ret < 0) goto cleanup_table; ret = nf_register_hook(&ipt_ops[1]); if (ret < 0) goto cleanup_hook0; ret = nf_register_hook(&ipt_ops[2]); if (ret < 0) goto cleanup_hook1; return ret; //如果注册失败,将已注册的钩子清除掉 cleanup_hook1: nf_unregister_hook(&ipt_ops[1]); cleanup_hook0: nf_unregister_hook(&ipt_ops[0]); cleanup_table: ipt_unregister_table(&packet_filter); return ret; }
二、表的注册 表的注册由函数ipt_register_table来完成, ipt_register_table(&packet_filter); 其参数packet_filter包含了待注册表的各个参数: static struct ipt_table packet_filter = { { NULL, NULL }, "filter", &initial_table.repl, FILTER_VALID_HOOKS, RW_LOCK_UNLOCKED, NULL, THIS_MODULE }; 可见,内核中,表是以结构struct ipt_table来表示的: struct ipt_table { struct list_head list; /* 用于构造,维护链表的结构 */ char name[IPT_TABLE_MAXNAMELEN]; /* 表名,如"filter"、"nat"等,为了满足自动模块加载的设计,包含该表的模块应命名为iptable_'name'.o */ struct ipt_replace *table; /* 表的初始化模板,初始为initial_table.repl */ unsigned int valid_hooks; /* 位向量,表示当前表所影响的HOOK */ rwlock_t lock; /* 读写锁,初始为打开状态 */ struct ipt_table_info *private; /* iptable的数据区*/ struct module *me; /* 是否在模块中定义,若否,则为NULL */ }; 对照这一结构分析,filter表的初始化为: 链表:{ NULL, NULL } 表名:"filter" 初始化模板:&initial_table.repl 当前表所影响的Hook:FILTER_VALID_HOOKS /*#define FILTER_VALID_HOOKS ((1 << NF_IP_LOCAL_IN) | (1 << NF_IP_FORWARD) | (1 << NF_IP_LOCAL_OUT))*/ 读写锁:RW_LOCK_UNLOCKED,即为打开状态 数据区: NULL 是否在模块中定义:THIS_MODULE,见如下宏定义: #ifndef THIS_MODULE #ifdef MODULE #define THIS_MODULE (&__this_module) #else #define THIS_MODULE (NULL) #endif #endif 先来看维护表的链表的结构: struct list_head { struct list_head *next, *prev; }; 很简单,它是一个双向链表。 另一个重要的东东就是表的模板和数据区。表模板定义了一个初始化用的该表的所默认的HOOK所包含的规则等信息,它被初始化成了 &initial_table.repl。而初始化的数据区struct ipt_table_info *private为空。这样,ipt_register_table()函数会用repl成员的 内容去填充private成员. struct ipt_table_info是实际描述表的数据结构(net/ipv4/netfilter/ip_tables.c): struct ipt_table_info { unsigned int size; /* 表大小 */ unsigned int number; /* 表中的规则数 */ unsigned int initial_entries; /* 初始的规则数,用于模块计数 */ unsigned int hook_entry[NF_IP_NUMHOOKS]; /* 记录所影响的HOOK的规则入口相对于下面的entries变量的偏移量 */ unsigned int underflow[NF_IP_NUMHOOKS]; /* 与hook_entry相对应的规则表上限偏移量,当无规则录入时,相应的hook_entry和underflow均为0 */ char entries[0] ____cacheline_aligned; /* 规则表入口 */ }; 再来看模板的定义,这个结构很简单,不过长了点: static struct { struct ipt_replace repl; struct ipt_standard entries[3]; struct ipt_error term; } initial_table __initdata = { { "filter", FILTER_VALID_HOOKS, 4, sizeof(struct ipt_standard) * 3 + sizeof(struct ipt_error), { [NF_IP_LOCAL_IN] 0, [NF_IP_FORWARD] sizeof(struct ipt_standard), [NF_IP_LOCAL_OUT] sizeof(struct ipt_standard) * 2 }, { [NF_IP_LOCAL_IN] 0, [NF_IP_FORWARD] sizeof(struct ipt_standard), [NF_IP_LOCAL_OUT] sizeof(struct ipt_standard) * 2 }, 0, NULL, { } }, { /* LOCAL_IN */ { { { { 0 }, { 0 }, { 0 }, { 0 }, "", "", { 0 }, { 0 }, 0, 0, 0 }, 0, sizeof(struct ipt_entry), sizeof(struct ipt_standard), 0, { 0, 0 }, { } }, { { { { IPT_ALIGN(sizeof(struct ipt_standard_target)), "" } }, { } }, -NF_ACCEPT - 1 } }, /* FORWARD */ { { { { 0 }, { 0 }, { 0 }, { 0 }, "", "", { 0 }, { 0 }, 0, 0, 0 }, 0, sizeof(struct ipt_entry), sizeof(struct ipt_standard), 0, { 0, 0 }, { } }, { { { { IPT_ALIGN(sizeof(struct ipt_standard_target)), "" } }, { } }, -NF_ACCEPT - 1 } }, /* LOCAL_OUT */ { { { { 0 }, { 0 }, { 0 }, { 0 }, "", "", { 0 }, { 0 }, 0, 0, 0 }, 0, sizeof(struct ipt_entry), sizeof(struct ipt_standard), 0, { 0, 0 }, { } }, { { { { IPT_ALIGN(sizeof(struct ipt_standard_target)), "" } }, { } }, -NF_ACCEPT - 1 } } }, /* ERROR */ { { { { 0 }, { 0 }, { 0 }, { 0 }, "", "", { 0 }, { 0 }, 0, 0, 0 }, 0, sizeof(struct ipt_entry), sizeof(struct ipt_error), 0, { 0, 0 }, { } }, { { { { IPT_ALIGN(sizeof(struct ipt_error_target)), IPT_ERROR_TARGET } }, { } }, "ERROR" } } }; 结构长了点,我们先来关心注册表时的初始值: &initial_table.repl 这是一个struct ipt_replace结构,该结构做为初始化模版被使用,同样用户通过系统调用更换 表时也要用到这个结构。定义如下: /* The argument to IPT_SO_SET_REPLACE. */ struct ipt_replace { /* 表名. */ char name[IPT_TABLE_MAXNAMELEN]; /* 该表所影响的Hook. */ unsigned int valid_hooks; /* Number of entries */ unsigned int num_entries; /* Total size of new entries */ unsigned int size; /* 记录所影响的HOOK的规则入口相对于下面的entries变量的偏移量 */ unsigned int hook_entry[NF_IP_NUMHOOKS]; /* 与hook_entry相对应的规则表上限偏移量,当无规则录入时,相应的hook_entry和underflow均为0 */ unsigned int underflow[NF_IP_NUMHOOKS]; /* Information about old entries: */ /* Number of counters (must be equal to current number of entries). */ unsigned int num_counters; /* The old entries' counters. */ struct ipt_counters *counters; /* The entries (hang off end: not really an array). */ struct ipt_entry entries[0]; }; 对照结构,可以分析各个成员的初始化值了: char name[IPT_TABLE_MAXNAMELEN]="filter"; unsigned int valid_hooks=FILTER_VALID_HOOKS; unsigned int num_entries=4; unsigned int size=sizeof(struct ipt_standard) * 3 + sizeof(struct ipt_error); unsigned int hook_entry[NF_IP_NUMHOOKS]={ [NF_IP_LOCAL_IN] 0, [NF_IP_FORWARD] sizeof(struct ipt_standard), [NF_IP_LOCAL_OUT] sizeof(struct ipt_standard) * 2 }; unsigned int underflow={ [NF_IP_LOCAL_IN] 0, [NF_IP_FORWARD] sizeof(struct ipt_standard), [NF_IP_LOCAL_OUT] sizeof(struct ipt_standard) * 2 }; unsigned int num_counters=0; struct ipt_counters *counters=NULL; struct ipt_entry entries[0]={ }; 了解了这些结构后,再来看表的注册函数: int ipt_register_table(struct ipt_table *table) { int ret; struct ipt_table_info *newinfo; static struct ipt_table_info bootstrap = { 0, 0, 0, { 0 }, { 0 }, { } }; /*宏MOD_INC_USE_COUNT用于模块计数器累加,主要是为了防止模块异常删除,对应的 宏MOD_DEC_USE_COUNT就是累减了*/ MOD_INC_USE_COUNT; /*为每个CPU分配规则空间*/ newinfo = vmalloc(sizeof(struct ipt_table_info) + SMP_ALIGN(table->table->size) * smp_num_cpus); /*分配失败*/ if (!newinfo) { ret = -ENOMEM; MOD_DEC_USE_COUNT; return ret; } /*将规则项拷贝到新表项的第一个cpu空间里面*/ memcpy(newinfo->entries, table->table->entries, table->table->size); /*translate_table函数将newinfo表示的table的各个规则进行边界检查,然后对于newinfo所指的 ipt_talbe_info结构中的hook_entries和underflows赋予正确的值,最后将表项向其他cpu拷贝*/ ret = translate_table(table->name, table->valid_hooks, newinfo, table->table->size, table->table->num_entries, table->table->hook_entry, table->table->underflow); if (ret != 0) { vfree(newinfo); MOD_DEC_USE_COUNT; return ret; } ret = down_interruptible(&ipt_mutex); if (ret != 0) { vfree(newinfo); MOD_DEC_USE_COUNT; return ret; } /* 如果注册的table已经存在,释放空间 并且递减模块计数 */ if (list_named_find(&ipt_tables, table->name)) { ret = -EEXIST; goto free_unlock; } /* 替换table项. */ table->private = &bootstrap; if (!replace_table(table, 0, newinfo, &ret)) goto free_unlock; duprintf("table->private->number = %u/n", table->private->number); /* 保存初始规则计数器 */ table->private->initial_entries = table->private->number; table->lock = RW_LOCK_UNLOCKED; /*将表添加进链表*/ list_prepend(&ipt_tables, table); unlock: up(&ipt_mutex); return ret; free_unlock: vfree(newinfo); MOD_DEC_USE_COUNT; goto unlock; } 呵呵,初次看table的注册,有点头大,因为它不光是netfilter,还涉及到linux内核中的内存管理、 信号量设置等等,不过其实注册也就完成两件事:初始化表,将表添加进表的链表。
表的注册中涉及到的重要函数 表注册函数中,主要涉及到的重要函数有: translate_table list_named_find list_prepend 1、translate_table /* * 函数:translate_table() * 参数: * name:表名称; * valid_hooks:当前表所影响的hook * newinfo:包含当前表的所有信息的结构 * size:表的大小 * number:表中的规则数 * hook_entries:记录所影响的HOOK的规则入口相对于下面的entries变量的偏移量 * underflows:与hook_entry相对应的规则表上限偏移量 * 作用: * translate_table函数将newinfo表示的table的各个规则进行边界检查,然后对于newinfo所指的 * ipt_talbe_info结构中的hook_entries和underflows赋予正确的值,最后将表项向其他cpu拷贝 * 返回值: * int ret==0表示成功返回 */ static int translate_table(const char *name, unsigned int valid_hooks, struct ipt_table_info *newinfo, unsigned int size, unsigned int number, const unsigned int *hook_entries, const unsigned int *underflows) { unsigned int i; int ret; newinfo->size = size; newinfo->number = number; /* 初始化所有Hooks为不可能的值. */ for (i = 0; i < NF_IP_NUMHOOKS; i++) { newinfo->hook_entry[i] = 0xFFFFFFFF; newinfo->underflow[i] = 0xFFFFFFFF; } duprintf("translate_table: size %u/n", newinfo->size); i = 0; /* 遍历所有规则,检查所有偏量,检查的工作都是由IPT_ENTRY_ITERATE这个宏来完成,并且它 的最后一个参数i,返回表的所有规则数. */ ret = IPT_ENTRY_ITERATE(newinfo->entries, newinfo->size, check_entry_size_and_hooks, newinfo, newinfo->entries, newinfo->entries + size, hook_entries, underflows, &i); if (ret != 0) return ret; /*实际计算得到的规则数与指定的不符*/ if (i != number) { duprintf("translate_table: %u not %u entries/n", i, number); return -EINVAL; } /* 因为函数一开始将HOOK的偏移地址全部初始成了不可能的值,而在上一个宏的遍历中设置了 hook_entries和underflows的值,这里对它们进行检查 */ for (i = 0; i < NF_IP_NUMHOOKS; i++) { /* 只检查当前表所影响的hook */ if (!(valid_hooks & (1 << i))) continue; if (newinfo->hook_entry[i] == 0xFFFFFFFF) { duprintf("Invalid hook entry %u %u/n", i, hook_entries[i]); return -EINVAL; } if (newinfo->underflow[i] == 0xFFFFFFFF) { duprintf("Invalid underflow %u %u/n", i, underflows[i]); return -EINVAL; } } /*确保新的table中不存在规则环*/ if (!mark_source_chains(newinfo, valid_hooks)) return -ELOOP; /* 对tables中的规则项进行完整性检查,保证每一个规则项在形式上是合法的*/ i = 0; ret = IPT_ENTRY_ITERATE(newinfo->entries, newinfo->size, check_entry, name, size, &i); /*检查失败,释放空间,返回*/ if (ret != 0) { IPT_ENTRY_ITERATE(newinfo->entries, newinfo->size, cleanup_entry, &i); return ret; } /* 为每个CPU复制一个完整的table项*/ for (i = 1; i < smp_num_cpus; i++) { memcpy(newinfo->entries + SMP_ALIGN(newinfo->size)*i, newinfo->entries, SMP_ALIGN(newinfo->size)); } return ret; } 函数的核心处理,是调用了IPT_ENTRY_ITERATE,我在《iptables源码分析》中已提过,这个宏用来遍历每一个规则,然后 调用其第三个参数(函数指针)进行处理,前两个参数分别表示规则的起始位置和规则总大小,后面的参数则视情况而定。 再来看一次: /* fn returns 0 to continue iteration */ #define IPT_ENTRY_ITERATE(entries, size, fn, args...) / ({ / unsigned int __i; / int __ret = 0; / struct ipt_entry *__entry; / / for (__i = 0; __i < (size); __i += __entry->next_offset) { / __entry = (void *)(entries) + __i; / / __ret = fn(__entry , ## args); / if (__ret != 0) / break; / } / __ret; / }) 对应地,函数的第一次宏的调用, ret = IPT_ENTRY_ITERATE(newinfo->entries, newinfo->size, check_entry_size_and_hooks, newinfo, newinfo->entries, newinfo->entries + size, hook_entries, underflows, &i); 遍历到每一项规则后,就调用check_entry_size_and_hooks继续处理。 static inline int check_entry_size_and_hooks(struct ipt_entry *e, struct ipt_table_info *newinfo, unsigned char *base, unsigned char *limit, const unsigned int *hook_entries, const unsigned int *underflows, unsigned int *i) { unsigned int h; /*(unsigned long)e % __alignof__(struct ipt_entry) != 0--不能整除,规则不完整 (unsigned char *)e + sizeof(struct ipt_entry) >= limit--超过上限了*/ if ((unsigned long)e % __alignof__(struct ipt_entry) != 0 || (unsigned char *)e + sizeof(struct ipt_entry) >= limit) { duprintf("Bad offset %p/n", e); return -EINVAL; } /*e->next_offset < sizeof(struct ipt_entry) + sizeof(struct ipt_entry_target)--规则太"短"了,小于最基本的长度 */ if (e->next_offset < sizeof(struct ipt_entry) + sizeof(struct ipt_entry_target)) { duprintf("checking: element %p size %u/n", e, e->next_offset); return -EINVAL; } /* 检查并设置正确的 hooks & underflows */ for (h = 0; h < NF_IP_NUMHOOKS; h++) { if ((unsigned char *)e - base == hook_entries[h]) newinfo->hook_entry[h] = hook_entries[h]; if ((unsigned char *)e - base == underflows[h]) newinfo->underflow[h] = underflows[h]; } /* FIXME: underflows must be unconditional, standard verdicts < 0 (not IPT_RETURN). --RR */ /* Clear counters and comefrom */ e->counters = ((struct ipt_counters) { 0, 0 }); /*包和字节的计数器清零*/ e->comefrom = 0; /*环路计数器清零*/ (*i)++; /*规则计数器累加*/ return 0; } 2、replace_table 前面说过,表中以struct ipt_table_info *private;表示实际数据区。但是在初始化赋值的时候,被设为 NULL,而表的初始变量都以模版的形式,放在struct ipt_replace *table;中。 注册函数一开始,就声明了: struct ipt_table_info *newinfo; 然后对其分配了空间,将模块中的初值拷贝了进来。所以replace_table要做的工作,主要就是把newinfo中的 值传递给table结构中的private成员。 其函数原型如下: static struct ipt_table_info * replace_table(struct ipt_table *table, unsigned int num_counters, struct ipt_table_info *newinfo, int *error) { struct ipt_table_info *oldinfo; #ifdef CONFIG_NETFILTER_DEBUG { struct ipt_entry *table_base; unsigned int i; for (i = 0; i < smp_num_cpus; i++) { table_base = (void *)newinfo->entries + TABLE_OFFSET(newinfo, i); table_base->comefrom = 0xdead57ac; } } #endif /* Do the substitution. */ write_lock_bh(&table->lock); /* Check inside lock: is the old number correct? */ if (num_counters != table->private->number) { duprintf("num_counters != table->private->number (%u/%u)/n", num_counters, table->private->number); write_unlock_bh(&table->lock); *error = -EAGAIN; return NULL; } oldinfo = table->private; table->private = newinfo; newinfo->initial_entries = oldinfo->initial_entries; write_unlock_bh(&table->lock); return oldinfo; } 3、list_named_find 在注册函数中,调用 /* Don't autoload: we'd eat our tail... */ if (list_named_find(&ipt_tables, table->name)) { ret = -EEXIST; goto free_unlock; } 来检查当前表是否已被注册过了。可见,第一个参数为链表首部,第二个参数为当前表名。 其原型如下: /* Find this named element in the list. */ #define list_named_find(head, name) / LIST_FIND(head, __list_cmp_name, void *, name) /* Return pointer to first true entry, if any, or NULL. A macro required to allow inlining of cmpfn. */ #define LIST_FIND(head, cmpfn, type, args...) / ({ / const struct list_head *__i = (head); / / ASSERT_READ_LOCK(head); / do { / __i = __i->next; / if (__i == (head)) { / __i = NULL; / break; / } / } while (!cmpfn((const type)__i , ## args)); / (type)__i; / }) 前面提过,表是一个双向链表,在宏当中,以while进行循环,以__i = __i->next; 进行遍历,然后调用比较函数进行比较,传递过来的比较函数是__list_cmp_name。 比较函数很简单: static inline int __list_cmp_name(const void *i, const char *name) { return strcmp(name, i+sizeof(struct list_head)) == 0; } 4、list_prepend 当所有的初始化工作结束,就调用list_prepend来构建链表了。 /* Prepend. */ static inline void list_prepend(struct list_head *head, void *new) { ASSERT_WRITE_LOCK(head); /*设置写互斥*/ list_add(new, head); /*将当前表节点添加进链表*/ } list_add就是一个构建双向链表的过程: static __inline__ void list_add(struct list_head *new, struct list_head *head) { __list_add(new, head, head->next); } static __inline__ void __list_add(struct list_head * new, struct list_head * prev, struct list_head * next) { next->prev = new; new->next = next; new->prev = prev; prev->next = new; }
三、Hook的注册 如果你对Netfilter的hook的注册还不了解的话,推荐先到网上搜搜《深入Linux网络核心堆栈》bioforge <alkerr@yifan.net> 看看先。(本节中有部份文字引自该文) 注册一个hook函数是围绕nf_hook_ops数据结构的一个非常简单的操作,nf_hook_ops数据结构在linux/netfilter.h中定义, 该数据结构的定义如下: struct nf_hook_ops { struct list_head list; /* User fills in from here down. */ nf_hookfn *hook; int pf; int hooknum; /* Hooks are ordered in ascending priority. */ int priority; }; 该数据结构中的list成员用于维护Netfilter hook的列表。 hook成员是一个指向nf_hookfn类型的函数的指针,该函数是这个hook被调用时执行的函数。 nf_hookfn同样在linux/netfilter.h中定义。 pf这个成员用于指定协议族。有效的协议族在linux/socket.h中列出,但对于IPv4我们希望使用协议族PF_INET。 hooknum这个成员用于指定安装的这个函数对应的具体的hook类型: NF_IP_PRE_ROUTING 在完整性校验之后,选路确定之前 NF_IP_LOCAL_IN 在选路确定之后,且数据包的目的是本地主机 NF_IP_FORWARD 目的地是其它主机地数据包 NF_IP_LOCAL_OUT 来自本机进程的数据包在其离开本地主机的过程中 NF_IP_POST_ROUTING 在数据包离开本地主机“上线”之前 最后,priority这个成员用于指定在执行的顺序中,这个hook函数应当在被放在什么地方。 对于IPv4,可用的值在linux/netfilter_ipv4.h的nf_ip_hook_priorities枚举中定义。 针对HOOK的注册,在初始化函数中有: /* Register table */ ret = ipt_register_table(&packet_filter); if (ret < 0) return ret; /* Register hooks */ ret = nf_register_hook(&ipt_ops[0]); if (ret < 0) goto cleanup_table; ret = nf_register_hook(&ipt_ops[1]); if (ret < 0) goto cleanup_hook0; ret = nf_register_hook(&ipt_ops[2]); if (ret < 0) goto cleanup_hook1; 可见,注册是通过nf_register_hook函数来完成,每一个Hook的相关信息,都在ipt_ops结构数组中,它的成员变量前面已做分析, 来看看它的初始化值: static struct nf_hook_ops ipt_ops[] = { { { NULL, NULL }, ipt_hook, PF_INET, NF_IP_LOCAL_IN, NF_IP_PRI_FILTER }, { { NULL, NULL }, ipt_hook, PF_INET, NF_IP_FORWARD, NF_IP_PRI_FILTER }, { { NULL, NULL }, ipt_local_out_hook, PF_INET, NF_IP_LOCAL_OUT, NF_IP_PRI_FILTER } }; 对应结构各成员变量的含义,可见,filter表上总共设置了NF_IP_LOCAL_IN,NF_IP_FORWARD,NF_IP_LOCAL_OUT,用熟了iptables三个 链,对这三个东东应该是刻骨铭心了。协议簇是PF_INET,初始化链表为NULL,处理函数,前两个为ipt_hook,后一个为ipt_local_out_hook, 优化级均为NF_IP_PRI_FILTER。 hook的注册,是通过nf_register_hook来完成的,它也是一个维护双向链表的过程,值得注意的是,注册的钩子函数,全部是放在全局变量 nf_hooks中,它是一个二维数组,函数一开始先遍历它,找到合适的地方,再将当前节点插入之。(我们可以想像,将来调用钩子函数时,就 是一个查找nf_hooks数组成员的过程) int nf_register_hook(struct nf_hook_ops *reg) { struct list_head *i; br_write_lock_bh(BR_NETPROTO_LOCK); /*寻找与当前待注册节点reg匹配的数组元素(按协议族和Hook来匹配)*/ for (i = nf_hooks[reg->pf][reg->hooknum].next; i != &nf_hooks[reg->pf][reg->hooknum]; i = i->next) { if (reg->priority < ((struct nf_hook_ops *)i)->priority) break; } /*添加节点*/ list_add(®->list, i->prev); br_write_unlock_bh(BR_NETPROTO_LOCK); return 0; } 能过表的注册,HOOK的注册,准备工作基本上就完成了,其它表的注册和Hook的注册,都是一样的,可以对照分析,没有必要再详述了。 不过注册也只是准备工作。重要的事情是对数据包的处理,对于filter来说,就是包过滤,对于nat来讲,就是地址转换。 四、数据包过滤 1、钩子函数 以中转包过滤为例(FORWARD),注册的时候,向内核注册了一个ipt_hook的钩子函数。 static unsigned int ipt_hook(unsigned int hook, //Hook类型 struct sk_buff **pskb, //数据包 const struct net_device *in, //进入数据包接口 const struct net_device *out, //离开数据包接口 int (*okfn)(struct sk_buff *)) //默认处理函数 { return ipt_do_table(pskb, hook, in, out, &packet_filter, NULL); } 转向到了ipt_do_table。也就是说,如果向内核挂了钩,中转的数据,将进入ipt_do_table函数。 2、钩子函数被调用 钩子函数被注册了,但是内核是如何调用它的呢? 在/src/net/ipv4下边,对应于input/output/forward,分别有Ip_forward.c,Ip_output.c,Ip_input.c。同样继续以forward为例, (关于linux堆栈处理数据包流程的各个函数的作用等,这里就不进一步详述,请参考其它相关资料)。 对于转发的数据,将进入Ip_forward.c中的ip_forward函数,当处理完成后,在最后一句,可以看到: return NF_HOOK(PF_INET, NF_IP_FORWARD, skb, skb->dev, dev2, ip_forward_finish); 事实上,你在linux的每一个数据转发的"关节"的函数处,都可以发现这个宏的调用,它就是调用我们注册的钩子,其最后一个参数为 下一步处理的函数,即,如果有钩子函数,则处理完所有的钩子函数后,调用这个函数继续处理,如果没有注册任何钩子,则直接调用 此函数。 /* This is gross, but inline doesn't cut it for avoiding the function call in fast path: gcc doesn't inline (needs value tracking?). --RR */ #ifdef CONFIG_NETFILTER_DEBUG #define NF_HOOK nf_hook_slow #else #define NF_HOOK(pf, hook, skb, indev, outdev, okfn) / (list_empty(&nf_hooks[(pf)][(hook)]) / ? (okfn)(skb) / : nf_hook_slow((pf), (hook), (skb), (indev), (outdev), (okfn))) #endif 先初略看看这个宏,okfn,我们已讲过,它是下一步要处理的函数,这里先调用 list_empty函数检查nf_hooks是否为空,为空则表示没有Hook注册,则直接调用 okfn继续处理。如果不为空,则转入nf_hook_slow函数: int nf_hook_slow(int pf, unsigned int hook, struct sk_buff *skb, struct net_device *indev, struct net_device *outdev, int (*okfn)(struct sk_buff *)) { struct list_head *elem; unsigned int verdict; int ret = 0; /* This stopgap cannot be removed until all the hooks are audited. */ if (skb_is_nonlinear(skb) && skb_linearize(skb, GFP_ATOMIC) != 0) { kfree_skb(skb); return -ENOMEM; } if (skb->ip_summed == CHECKSUM_HW) { if (outdev == NULL) { skb->ip_summed = CHECKSUM_NONE; } else { skb_checksum_help(skb); } } /* We may already have this, but read-locks nest anyway */ br_read_lock_bh(BR_NETPROTO_LOCK); #ifdef CONFIG_NETFILTER_DEBUG if (skb->nf_debug & (1 << hook)) { printk("nf_hook: hook %i already set./n", hook); nf_dump_skb(pf, skb); } skb->nf_debug |= (1 << hook); #endif /*因为在调用NF_HOOK宏时,已经指定了协议簇和钩子名称,所以要找到对应的Hook点,是很容易的 elem即为我们要找的,记得struct nf_hook_ops结构么?双向链表中的每个elem->hook就是我们关心的终极目标*/ elem = &nf_hooks[pf][hook]; /*找到后,遍历双向链表,进一步处理,以调用Hook函数,并返回相应的动作*/ verdict = nf_iterate(&nf_hooks[pf][hook], &skb, hook, indev, outdev, &elem, okfn); if (verdict == NF_QUEUE) { NFDEBUG("nf_hook: Verdict = QUEUE./n"); nf_queue(skb, elem, pf, hook, indev, outdev, okfn); } /*如果是接受,则调用okfn继续处理,否则丢度之*/ switch (verdict) { case NF_ACCEPT: ret = okfn(skb); break; case NF_DROP: kfree_skb(skb); ret = -EPERM; break; } br_read_unlock_bh(BR_NETPROTO_LOCK); return ret; } 再来看nf_iterate: static unsigned int nf_iterate(struct list_head *head, struct sk_buff **skb, int hook, const struct net_device *indev, const struct net_device *outdev, struct list_head **i, int (*okfn)(struct sk_buff *)) { /*循环遍历所有注册的钩子函数,包括系统默认的三个,用户自定义的……*/ for (*i = (*i)->next; *i != head; *i = (*i)->next) { struct nf_hook_ops *elem = (struct nf_hook_ops *)*i; /*就在这里调用了*/ switch (elem->hook(hook, skb, indev, outdev, okfn)) { case NF_QUEUE: return NF_QUEUE; case NF_STOLEN: return NF_STOLEN; case NF_DROP: return NF_DROP; case NF_REPEAT: *i = (*i)->prev; break; #ifdef CONFIG_NETFILTER_DEBUG case NF_ACCEPT: break; default: NFDEBUG("Evil return from %p(%u)./n", elem->hook, hook); #endif } } return NF_ACCEPT; } 解释一下各个返回值: NF_DROP 丢弃该数据包 NF_ACCEPT 保留该数据包 NF_STOLEN 忘掉该数据包 NF_QUEUE 将该数据包插入到用户空间 NF_REPEAT 再次调用该hook函数 这样,最终关心的还是每一个注册的函数,这样又回到本节开头所说的ipt_do_table…… 说了这么多,其实只是把最简单,最没有用的讲了,难的还在于Hook函数,呵呵命运的魔轮已经开启,每一个数据包的命运将会如何? 那就要去分析每一个Hook函数了……
我写出来等着大家来批判,来继续写下去,来指正我的许多错误,不过好像没有人响应,牛人们都对这个没有兴趣……? 哎,那我就接着写吧。 前面说到,对于filter表来说,所有的一切,要靠ipt_do_table函数来进行包配备,前面提过,过滤规则分为三部份:标准mathc,扩展match,target。可以预想一想,ipt_do_table就是要针对这三部份来过滤,来看一下该函数: /* Returns one of the generic firewall policies, like NF_ACCEPT. */ unsigned int ipt_do_table(struct sk_buff **pskb, unsigned int hook, const struct net_device *in, const struct net_device *out, struct ipt_table *table, void *userdata) { static const char nulldevname[IFNAMSIZ] = { 0 }; u_int16_t offset; struct iphdr *ip; void *protohdr; u_int16_t datalen; int hotdrop = 0; /* Initializing verdict to NF_DROP keeps gcc happy. */ unsigned int verdict = NF_DROP; const char *indev, *outdev; void *table_base; struct ipt_entry *e, *back; /* Initialization */ ip = (*pskb)->nh.iph; protohdr = (u_int32_t *)ip + ip->ihl; datalen = (*pskb)->len - ip->ihl * 4; indev = in ? in->name : nulldevname; outdev = out ? out->name : nulldevname; /* We handle fragments by dealing with the first fragment as * if it was a normal packet. All other fragments are treated * normally, except that they will NEVER match rules that ask * things we don't know, ie. tcp syn flag or ports). If the * rule is also a fragment-specific rule, non-fragments won't * match it. */ offset = ntohs(ip->frag_off) & IP_OFFSET; read_lock_bh(&table->lock); IP_NF_ASSERT(table->valid_hooks & (1 << hook)); table_base = (void *)table->private->entries + TABLE_OFFSET(table->private, cpu_number_map(smp_processor_id())); e = get_entry(table_base, table->private->hook_entry[hook]); #ifdef CONFIG_NETFILTER_DEBUG /* Check noone else using our table */ if (((struct ipt_entry *)table_base)->comefrom != 0xdead57ac && ((struct ipt_entry *)table_base)->comefrom != 0xeeeeeeec) { printk("ASSERT: CPU #%u, %s comefrom(%p) = %X/n", smp_processor_id(), table->name, &((struct ipt_entry *)table_base)->comefrom, ((struct ipt_entry *)table_base)->comefrom); } ((struct ipt_entry *)table_base)->comefrom = 0x57acc001; #endif /* For return from builtin chain */ back = get_entry(table_base, table->private->underflow[hook]); do { IP_NF_ASSERT(e); IP_NF_ASSERT(back); (*pskb)->nfcache |= e->nfcache; if (ip_packet_match(ip, indev, outdev, &e->ip, offset)) { struct ipt_entry_target *t; if (IPT_MATCH_ITERATE(e, do_match, *pskb, in, out, offset, protohdr, datalen, &hotdrop) != 0) goto no_match; ADD_COUNTER(e->counters, ntohs(ip->tot_len), 1); t = ipt_get_target(e); IP_NF_ASSERT(t->u.kernel.target); /* Standard target? */ if (!t->u.kernel.target->target) { int v; v = ((struct ipt_standard_target *)t)->verdict; if (v < 0) { /* Pop from stack? */ if (v != IPT_RETURN) { verdict = (unsigned)(-v) - 1; break; } e = back; back = get_entry(table_base, back->comefrom); continue; } if (table_base + v != (void *)e + e->next_offset) { /* Save old back ptr in next entry */ struct ipt_entry *next = (void *)e + e->next_offset; next->comefrom = (void *)back - table_base; /* set back pointer to next entry */ back = next; } e = get_entry(table_base, v); } else { /* Targets which reenter must return abs. verdicts */ #ifdef CONFIG_NETFILTER_DEBUG ((struct ipt_entry *)table_base)->comefrom = 0xeeeeeeec; #endif verdict = t->u.kernel.target->target(pskb, hook, in, out, t->data, userdata); #ifdef CONFIG_NETFILTER_DEBUG if (((struct ipt_entry *)table_base)->comefrom != 0xeeeeeeec && verdict == IPT_CONTINUE) { printk("Target %s reentered!/n", t->u.kernel.target->name); verdict = NF_DROP; } ((struct ipt_entry *)table_base)->comefrom = 0x57acc001; #endif /* Target might have changed stuff. */ ip = (*pskb)->nh.iph; protohdr = (u_int32_t *)ip + ip->ihl; datalen = (*pskb)->len - ip->ihl * 4; if (verdict == IPT_CONTINUE) e = (void *)e + e->next_offset; else /* Verdict */ break; } } else { no_match: e = (void *)e + e->next_offset; } } while (!hotdrop); #ifdef CONFIG_NETFILTER_DEBUG ((struct ipt_entry *)table_base)->comefrom = 0xdead57ac; #endif read_unlock_bh(&table->lock); #ifdef DEBUG_ALLOW_ALL return NF_ACCEPT; #else if (hotdrop) return NF_DROP; else return verdict; #endif } 再来一句句看这个函数吧: 先是把该取的值取出来: ip = (*pskb)->nh.iph; /*获取IP头*/ protohdr = (u_int32_t *)ip + ip->ihl; /*跳过IP头,搞不明白,为什么不用( u_int8_t * )ip + ip->ihl << 2^o^*/ datalen = (*pskb)->len - ip->ihl * 4; /*指向数据区*/ indev = in ? in->name : nulldevname; /*取得输入设备名*/ outdev = out ? out->name : nulldevname; /*取得输出设备名*/ offset = ntohs(ip->frag_off) & IP_OFFSET; /*设置分片包的偏移*/ read_lock_bh(&table->lock); /*设置互斥锁*/ IP_NF_ASSERT(table->valid_hooks & (1 << hook)); /*检验HOOK,debug用的*/ /*获取当前表的当前CPU的规则入口*/ table_base = (void *)table->private->entries + TABLE_OFFSET(table->private, cpu_number_map(smp_processor_id())); /*获得当前表的当前Hook的规则的起始偏移量*/ e = get_entry(table_base, table->private->hook_entry[hook]); /*获得当前表的当前Hook的规则的上限偏移量*/ back = get_entry(table_base, table->private->underflow[hook]); 然后进行规则的匹配,是在一个do...while中实现的: do { (*pskb)->nfcache |= e->nfcache; /*如果IP包匹配,就断续匹配下去,否则就跳到下一条规则*/ if (ip_packet_match(ip, indev, outdev, &e->ip, offset)) { } else { no_match: e = (void *)e + e->next_offset; } }while (!hotdrop); 标准的match匹配,即struct ipt_ip这部份,是通过调用函数ip_packet_match来实现的; 当 ip_packet_match匹配,则继续匹配下去,否则就跳到下一条规则(e = (void *)e + e->next_offset;) ip_packet_match放到一边,把来看看后面的情况:如果标准的match匹配了,则: 接着匹配扩展match if (IPT_MATCH_ITERATE(e, do_match, *pskb, in, out, offset, protohdr, datalen, &hotdrop) != 0) goto no_match; IPT_MATCH_ITERATE这个宏已经遇到很多次了,它的作用是遍历扩展的match,而实际执行的功能函数是 do_match OK,如果不匹配,则goto no_match;执行下条规则,否则: /*这个宏用来分别处理字节计数器和分组计数器这两个计数器*/ ADD_COUNTER(e->counters, ntohs(ip->tot_len), 1); /*获取规则的target的偏移地址*/ t = ipt_get_target(e); 然后接着匹备target: if (!t->u.kernel.target->target) { int v; v = ((struct ipt_standard_target *)t)->verdict; if (v < 0) { /* Pop from stack? */ if (v != IPT_RETURN) { verdict = (unsigned)(-v) - 1; break; } e = back; back = get_entry(table_base, back->comefrom); continue; } if (table_base + v != (void *)e + e->next_offset) { /* Save old back ptr in next entry */ struct ipt_entry *next = (void *)e + e->next_offset; next->comefrom = (void *)back - table_base; /* set back pointer to next entry */ back = next; } e = get_entry(table_base, v); } else { /* Targets which reenter must return abs. verdicts */ #ifdef CONFIG_NETFILTER_DEBUG ((struct ipt_entry *)table_base)->comefrom = 0xeeeeeeec; #endif verdict = t->u.kernel.target->target(pskb, hook, in, out, t->data, userdata); #ifdef CONFIG_NETFILTER_DEBUG if (((struct ipt_entry *)table_base)->comefrom != 0xeeeeeeec && verdict == IPT_CONTINUE) { printk("Target %s reentered!/n", t->u.kernel.target->name); verdict = NF_DROP; } ((struct ipt_entry *)table_base)->comefrom = 0x57acc001; #endif /* Target might have changed stuff. */ ip = (*pskb)->nh.iph; protohdr = (u_int32_t *)ip + ip->ihl; datalen = (*pskb)->len - ip->ihl * 4; if (verdict == IPT_CONTINUE) e = (void *)e + e->next_offset; else /* Verdict */ break; } 因为到目前为止,我们谈match/target的数据结构,只接触到用户态,对于内核态的,几乎没有怎么接触,所以要把它说清楚,可不是一件容易的事 情。(或者先分析用户态添加规则在内核中是如何实现的,了解了match和target的组织再来分析代码,更容易些)。不过,反正无论如何,还是一步步 地来, 先来看看标准match的匹配部份,即ip_packet_match函数
ip_packet_match函数,标准match部份的匹配 先来看看相关的数据结构,在内核中,标准的match是以struct ipt_ip 结构来表示的,它包含了一条规则最基本的部份: /* Yes, Virginia, you have to zero the padding. */ struct ipt_ip { /* 来源/目的地址 */ struct in_addr src, dst; /* 来源/目的地址的掩码 */ struct in_addr smsk, dmsk; /*输入输出网络接口*/ char iniface[IFNAMSIZ], outiface[IFNAMSIZ]; unsigned char iniface_mask[IFNAMSIZ], outiface_mask[IFNAMSIZ]; /* 协议, 0 = ANY */ u_int16_t proto; /* 标志字段 */ u_int8_t flags; /* 取反标志 */ u_int8_t invflags; }; 这样,再来看这部份的判断是一个很简单的事情了: /* Returns whether matches rule or not. */ static inline int ip_packet_match(const struct iphdr *ip, const char *indev, const char *outdev, const struct ipt_ip *ipinfo, int isfrag) { size_t i; unsigned long ret; /*定义一个宏,当bool和invflg的是一真一假的情况时,返回真。注意这里使用两个“!”的目的是使得这样计算后的值域只取0和1两个值*/ #define FWINV(bool,invflg) ((bool) ^ !!(ipinfo->invflags & invflg)) /*处理源和目标ip地址,这个if语句的意义是:到达分组的源ip地址经过掩码处理后与规则中的ip不匹配并且规则中 没有包含对ip地址的取反,或者规则中包含了对匹配地址的取反,但到达分组的源ip与规则中的ip地址匹配,if的第 一部分返回真,同样道理处理到达分组的目的ip地址。这两部分任意部分为真时,源或者目标地址不匹配。*/ if (FWINV((ip->saddr&ipinfo->smsk.s_addr) != ipinfo->src.s_addr, IPT_INV_SRCIP) || FWINV((ip->daddr&ipinfo->dmsk.s_addr) != ipinfo->dst.s_addr, IPT_INV_DSTIP)) { dprintf("Source or dest mismatch./n"); dprintf("SRC: %u.%u.%u.%u. Mask: %u.%u.%u.%u. Target: %u.%u.%u.%u.%s/n", NIPQUAD(ip->saddr), NIPQUAD(ipinfo->smsk.s_addr), NIPQUAD(ipinfo->src.s_addr), ipinfo->invflags & IPT_INV_SRCIP ? " (INV)" : ""); dprintf("DST: %u.%u.%u.%u Mask: %u.%u.%u.%u Target: %u.%u.%u.%u.%s/n", NIPQUAD(ip->daddr), NIPQUAD(ipinfo->dmsk.s_addr), NIPQUAD(ipinfo->dst.s_addr), ipinfo->invflags & IPT_INV_DSTIP ? " (INV)" : ""); return 0; } /*接着处理输入和输出的接口,for语句处理接口是否与规则中的接口匹配,不匹配时,ret返回非零,离开for语句后, 处理接口的取反问题:当接口不匹配并且接口不取反,或者接口匹配,但是接口取反,说明接口不匹配。*/ /*输入接口*/ for (i = 0, ret = 0; i < IFNAMSIZ/sizeof(unsigned long); i++) { ret |= (((const unsigned long *)indev) ^ ((const unsigned long *)ipinfo->iniface)) & ((const unsigned long *)ipinfo->iniface_mask); } if (FWINV(ret != 0, IPT_INV_VIA_IN)) { dprintf("VIA in mismatch (%s vs %s).%s/n", indev, ipinfo->iniface, ipinfo->invflags&IPT_INV_VIA_IN ?" (INV)":""); return 0; } /*输出接口*/ for (i = 0, ret = 0; i < IFNAMSIZ/sizeof(unsigned long); i++) { ret |= (((const unsigned long *)outdev) ^ ((const unsigned long *)ipinfo->outiface)) & ((const unsigned long *)ipinfo->outiface_mask); } if (FWINV(ret != 0, IPT_INV_VIA_OUT)) { dprintf("VIA out mismatch (%s vs %s).%s/n", outdev, ipinfo->outiface, ipinfo->invflags&IPT_INV_VIA_OUT ?" (INV)":""); return 0; } /* 检查协议是否匹配的情况 */ if (ipinfo->proto && FWINV(ip->protocol != ipinfo->proto, IPT_INV_PROTO)) { dprintf("Packet protocol %hi does not match %hi.%s/n", ip->protocol, ipinfo->proto, ipinfo->invflags&IPT_INV_PROTO ? " (INV)":""); return 0; } /* If we have a fragment rule but the packet is not a fragment * then we return zero */ /*处理分片包的匹配情况*/ if (FWINV((ipinfo->flags&IPT_F_FRAG) && !isfrag, IPT_INV_FRAG)) { dprintf("Fragment rule but not fragment.%s/n", ipinfo->invflags & IPT_INV_FRAG ? " (INV)" : ""); return 0; } return 1; }
前面关于match的那个问题解决了,自己来回复。搞了大半天,还是看代码不仔细…… 本节前面大部份为解决问题的思路,和前面的贴子有重复,可以直接跳到后半截看^o^。 内核中的match: 1、表与规则 内核中,表用struct ipt_table表示,其成员struct ipt_table_info *private;表示表的数据区,而private的成员 char entries[0] __attribute__((aligned(SMP_CACHE_BYTES)));;表示每个CPU的规则的入口; 2、规则 struct ipt_entry { struct ipt_ip ip; /*标准的match部份,如地址,网络接口等*/ /* 规则所关心的数据包的位置的标志,有些match使用了,有些没有用 */ unsigned int nfcache; /* target区的偏移,通常target区位于match区之后,而match区则在ipt_entry的末尾; 初始化为sizeof(struct ipt_entry),即假定没有match */ u_int16_t target_offset; /* 下一条规则相对于本规则的偏移,也即本规则所用空间的总和, 初始化为sizeof(struct ipt_entry)+sizeof(struct ipt_target),即没有match */ u_int16_t next_offset; /* 位向量,为发现规则中存在”环路“提供手段*/ unsigned int comefrom; /* 包和字节计数器. */ struct ipt_counters counters; /*target或者是match(如果存在)的起始位置 */ unsigned char elems[0]; }; 3、match的表示 A、用户态 struct iptables_match { /* Match链,初始为NULL */ struct iptables_match *next; /* Match名,和核心模块加载类似,作为动态链接库存在的Iptables Extension的命名规则为libipt_'name'.so */ ipt_chainlabel name; /*版本信息,一般设为NETFILTER_VERSION */ const char *version; /* Match数据的大小,必须用IPT_ALIGN()宏指定对界*/ size_t size; /*由于内核可能修改某些域,因此size可能与确切的用户数据不同,这时就应该把不会被改变的数据放在数据区的前面部分,而这里就应该填写被改变的数据区大小;一般来说,这个值和size相同*/ size_t userspacesize; /*当iptables要求显示当前match的信息时(比如iptables-m ip_ext -h),就会调用这个函数,输出在iptables程序的通用信息之后. */ void (*help)(void); /*初始化,在parse之前调用. */ void (*init)(struct ipt_entry_match *m, unsigned int *nfcache); /*扫描并接收本match的命令行参数,正确接收时返回非0,flags用于保存状态信息*/ int (*parse)(int c, char **argv, int invert, unsigned int *flags, const struct ipt_entry *entry, unsigned int *nfcache, struct ipt_entry_match **match); /* 前面提到过这个函数,当命令行参数全部处理完毕以后调用,如果不正确,应该 退出(exit_error())*/ void (*final_check)(unsigned int flags); /*当查询当前表中的规则时,显示使用了当前match的规则*/ void (*print)(const struct ipt_ip *ip, const struct ipt_entry_match *match, int numeric); /*按照parse允许的格式将本match的命令行参数输出到标准输出,用于iptables-save命令. */ void (*save)(const struct ipt_ip *ip, const struct ipt_entry_match *match); /* NULL结尾的参数列表,struct option与getopt(3)使用的结构相同*/ const struct option *extra_opts; /* Ignore these men behind the curtain: */ unsigned int option_offset; struct ipt_entry_match *m; unsigned int mflags; unsigned int used; #ifdef NO_SHARED_LIBS unsigned int loaded; /* simulate loading so options are merged properly */ #endif }; 成员指针m是一个struct ipt_entry_match类型,这个东东后面再分析。 B、内核中,核心用struct ipt_match表征一个Match数据结构: struct ipt_match { /* 组织链表的成员,通常初始化成{NULL,NULL},由核心使用 */ struct list_head list; /* Match的名字*/ const char name[IPT_FUNCTION_MAXNAMELEN]; /*指向该Match的匹配函数,返回非0表示匹配成功,如果返回0且hotdrop设为1,则表示该报文应当立刻丢弃*/ int (*match)(const struct sk_buff *skb, const struct net_device *in, const struct net_device *out, const void *matchinfo, int offset, const void *hdr, u_int16_t datalen, int *hotdrop); /* 在使用本Match的规则注入表中之前调用,进行有效性检查,如果返回0,规则就不会加入iptables中 */ int (*checkentry)(const char *tablename, const struct ipt_ip *ip, void *matchinfo, unsigned int matchinfosize, unsigned int hook_mask); /* 在包含本Match的规则从表中删除时调用,与checkentry配合可用于动态内存分配和释放 */ void (*destroy)(void *matchinfo, unsigned int matchinfosize); /* 表示当前Match是否为模块(NULL为否) */ struct module *me; }; 因为Match都是以模块的形式存在,这两个结构分别在iptables/Netfilter的模块初始化函数注册时被使用。 struct ipt_entry_match结构非常重要,它把内核态与用户态关连起来。如果说前面两个关于match的结构用来做 match的抽像点的处理的话,那么struct ipt_entry_match则表示了规则中具体的每个match,即规则中存储的一条规则是: ipt_entry+ipt_entry_match1+ipt_entry_match2+ipt_entry_match3…… struct ipt_entry_match { union { struct { u_int16_t match_size; /* Used by userspace */ char name[IPT_FUNCTION_MAXNAMELEN]; } user; struct { u_int16_t match_size; /* Used inside the kernel */ struct ipt_match *match; } kernel; /* Total length */ u_int16_t match_size; } u; unsigned char data[0]; }; 而在匹配每一条规则时: /* fn returns 0 to continue iteration */ #define IPT_MATCH_ITERATE(e, fn, args...) / ({ / unsigned int __i; / int __ret = 0; / struct ipt_entry_match *__match; / / for (__i = sizeof(struct ipt_entry); / __i < (e)->target_offset; / __i += __match->u.match_size) { / __match = (void *)(e) + __i; / / __ret = fn(__match , ## args); / if (__ret != 0) / break; / } / __ret; / }) 宏IPT_MATCH_ITERATE用来遍历规则中的每一个match,i用来做循环变量。 struct ipt_entry用来表示一条规则,最后一个成员unsigned char elems[0];用来紧跟 match和target,那么match的起始位置自然是: __i = sizeof(struct ipt_entry);即跳过ipt_entry target_offset表示规则中target的偏移位,即match的结束,所以i的结束自然为: __i < (e)->target_offset; 而每次步增的空间大小为match的大小,结构struct ipt_entry_match中以成员u.match_size表示当前match的大小,即: __i += __match->u.match_size i是每个match的偏移量,那么(void *)(e) + __i;则为每个match的绝对地址。 问题有又回到起始点上来了:规则匹配的时候,每条规则的__match.u.kernel.match 是如何与内核模块中每个match初始化 注册时建立链表的struct ipt_match东东关连起来的? 先来看看用户空间添加一条规则(虽然觉得极不可能是用户空间来做这件事情,报着侥幸的心理) 用户空间调用(iptables1.2.7 源码,Iptables.c:1653): switch (command) { case CMD_APPEND: ret = append_entry(……); append_entry iptc_append_entry insert_rules一路下来直接处理规则了,并没有单独的match的处理,看来不是用户态完成这一工作了。再来看看内核中: 发现在内核添加规则之前,会调用函数translate_table来进行检查和传递用户空间传递过来的规则: 其中有一句: static int translate_table(const char *name, unsigned int valid_hooks, struct ipt_table_info *newinfo, unsigned int size, unsigned int number, const unsigned int *hook_entries, const unsigned int *underflows) { /*linux2.4.20 Ip_tables.c 318行*/ ret = IPT_ENTRY_ITERATE(newinfo->entries, newinfo->size, check_entry, name, size, &i); } IPT_ENTRY_ITERATE这个宏前面分析过,用来遍历每一条规则,而check_entry为处理函数,在 check_entry函数中,又有如下语句: static inline int check_entry(struct ipt_entry *e, const char *name, unsigned int size, unsigned int *i) { ret = IPT_MATCH_ITERATE(e, check_match, name, &e->ip, e->comefrom, &j); } IPT_MATCH_ITERATE宏用来遍历某一条规则中的各个Match,check_matck为遍历到后的进一步处理函数。 再来看check_matck: static inline int check_match(struct ipt_entry_match *m, const char *name, const struct ipt_ip *ip, unsigned int hookmask, unsigned int *i) { int ret; struct ipt_match *match; /*根据规则中Match的名称,在已注册好的ipt_match双向链表中查找对应接点——已经接近要找的目标了*/ match = find_match_lock(m->u.user.name, &ret, &ipt_mutex); if (!match) { duprintf("check_match: `%s' not found/n", m->u.user.name); return ret; } if (match->me) __MOD_INC_USE_COUNT(match->me); /*晕,找的就是它了,找了大半天*/ m->u.kernel.match = match; up(&ipt_mutex); if (m->u.kernel.match->checkentry && !m->u.kernel.match->checkentry(name, ip, m->data, m->u.match_size - sizeof(*m), hookmask)) { if (m->u.kernel.match->me) __MOD_DEC_USE_COUNT(m->u.kernel.match->me); duprintf("ip_tables: check failed for `%s'./n", m->u.kernel.match->name); return -EINVAL; } (*i)++; return 0; } 知道了关连,理解do_match函数就不再是问题了: static inline int do_match(struct ipt_entry_match *m, const struct sk_buff *skb, const struct net_device *in, const struct net_device *out, int offset, const void *hdr, u_int16_t datalen, int *hotdrop) { /* Stop iteration if it doesn't match */ if (!m->u.kernel.match->match (skb, in, out, m->data, offset, hdr, datalen, hotdrop)) return 1; else return 0; }
filter表的内容,还剩下三个内容: 1、target的匹配; 2、每个模块的target/match等函数的实现; 3、内核与用户空间的交互; 这里,以target的匹配做为2005的的结尾吧(因为明天飞贵州,估计2005年是没有机会再发贴了) 注:这里说匹配,其实不太正确,因为前面match是条件,匹配条件是正常的,target是动作,应该用执行更准确些。 target的匹配 要理解target的匹配,还是需要先了解相关的数据结构。 与match相似,内核中每个target模块,通过一个struct ipt_target来实现: /* Registration hooks for targets. */ struct ipt_target { struct list_head list; /*target链,初始为NULL*/ const char name[IPT_FUNCTION_MAXNAMELEN]; /*target名称*/ /*target的模块函数,如果需要继续处理则返回IPT_CONTINUE(-1),否则返回NF_ACCEPT、NF_DROP等值, 它的调用者根据它的返回值来判断如何处理它处理过的报文*/ unsigned int (*target)(struct sk_buff **pskb, unsigned int hooknum, const struct net_device *in, const struct net_device *out, const void *targinfo, void *userdata); /* Called when user tries to insert an entry of this type: hook_mask is a bitmask of hooks from which it can be called. */ /* 在使用本Match的规则注入表中之前调用,进行有效性检查,如果返回0,规则就不会加入iptables中 */ int (*checkentry)(const char *tablename, const struct ipt_entry *e, void *targinfo, unsigned int targinfosize, unsigned int hook_mask); /* 在包含本Target的规则从表中删除时调用,与checkentry配合可用于动态内存分配和释放 */ void (*destroy)(void *targinfo, unsigned int targinfosize); /* 表示当前Target是否为模块(NULL为否) */ struct module *me; }; 这个结构样子与match的内核模块的描述几乎是一模一样了…… 而内核及用户态中,具体地存储描述一个target,是通过一个struct ipt_entry_target来实现的: struct ipt_entry_target { union { struct { u_int16_t target_size; /* Used by userspace */ char name[IPT_FUNCTION_MAXNAMELEN]; } user; struct { u_int16_t target_size; /* Used inside the kernel */ struct ipt_target *target; } kernel; /* Total length */ u_int16_t target_size; } u; unsigned char data[0]; }; 这个结构也与match一模一样,那么我们是不是就可以按照分析match的思路来分析match呢?“通过成员struct ipt_target *target;来与内核中注册的target的处理模块建立关连,再来调用u.kernel.target->target()函数进行匹配 ”??? 先不急,Netfilter的target共分为三类:内建动作、扩展target和用户自定义链。而以上两个结构是不够的,它们只能描述基于扩展 target的匹配函数,没有或可以讲至少没有显著地描述一个内建动作或者是用户自定义链!事实上,Netfilter描述一个完整的target,是通 过以下结构来实现的: struct ipt_standard_target { struct ipt_entry_target target; /*模块函数*/ int verdict; /*常数*/ }; 其中成员verdict(判断、判决)表明用来针对内建动作(ACCEPT、DROP)或者是用户自定义链,如果是扩展的target,则通过其target成员去定位最终的模块处理函数。 那么,问题又接踵而至了,如果内核中,模块也是以类似注册/维护双向链表的形式储备,那么在内核中匹配的时候如何来区分这三类target? 事实上,考虑到程序的通用性、扩展性,对于内建动作或者是用户自定义链,内核是采用了“假注册”的方式来处理(这个名字是偶私人取的,或许不正确),也就 是说,把内建动作或者是用户自定义链和扩展的target采用一样的处册方式,只是这个注册,只是一个样子,不具备实质意义: 在标准模块初始化Ip_tables.c的init函数注册target 的时候,可以看到: static int __init init(void) { int ret; /* Noone else will be downing sem now, so we won't sleep */ down(&ipt_mutex); list_append(&ipt_target, &ipt_standard_target); …… 其注册的成员ipt_standard_target表示“标准的target”,即前文所提到的内建动作和用户自定义链。我们来看看它的初始化值: /* The built-in targets: standard (NULL) and error. */ static struct ipt_target ipt_standard_target = { { NULL, NULL }, IPT_STANDARD_TARGET, NULL, NULL, NULL }; 同样,它也是一个ipt_target结构,也就是说和其它扩展的target模块一样,但是,它的处理函数全为空的,如target函数指针。所以,匹 配的时候,如果要匹分的话,可以判断此指针函数是否指向NULL即可。而至于在标准的target中区分内建动作还是用户自定议链,前面提到过,它们都是 以struct ipt_standard_target结构的verdict成员描述。到时候来判断verdict的值就可以了。我们可以推断出这段匹配的算法应该大致 如下: do { …… /*前面为匹配match部份,前几节已分析过了*/ 假设e为当前待匹配规则。 t=get_current_target(e); /*获取当前规则的当前target*/ /*因为如果注册时,如果是标准target,则t->u.kernel.target->target==NULL*/ if (!t->u.kernel.target->target) /*如果是标准target*/ { /*进入标准target的话,还要来区分究竟是内建的动作,还是用户自定链,前面分析 struct ipt_standard_target时说过,它们都是以verdict成员来描述的,则*/ if(判断verdict==内建动作) { /*相应处理*/ } else { /*相应处理*/ } } else { /*如果是扩展的target,调用target函数*/ verdict = t->u.kernel.target->target(); } }while(……); 就是在规则的循环匹配中,先根据target函数指针的值判断target,如果是标准的target,再根据的值匹别是内建动作还是自定义链。 程序实际的代码与此已经很相似了,唯一的区别在于程序在处理自定义链时有一些技巧。 回到struct ipt_standard_target的verdict成员上来,这是一个非常重要的东西。用户空间表示一个接受动作,使用ACCEPT,内核不用能这 个字符串来匹配,!strcmp(target.name,"ACCEPT"),这样效率差了点,一个自然的想法是,用一些整形数来代替,就像我们平常用 1来代替true,0来代替false一样。 链中还有一种规则的target,形如-j 自定义链名,内核中的规则,并没有直接的“链”的概念,是呈一维线性排例的,所以,需要跳转至自定义链时,就需要两个东东: 1、待跳转的链相对于这条-j 自定义链的偏移值; 2、回指针,跳完了,总要回来吧……并且,规则中-j RETURN这种规则,它同样也需要回指针; 对于一条默认链来讲: back = get_entry(table_base, table->private->underflow[hook]); 最初回指针是指向这条链的末尾处的。 OK,再回到偏移值的问题上来,内核仍然以verdict这个成员来描述这个偏移值,刚才说过用verdict来描述ACCEPT等这些内建动作,难道它们不会冲突,答案是否定的。内核约定:以负数来描述ACCEPT等内建动作,需要用到时,再取其正值就行了。 例如: #define NF_ACCEPT 1 /*内核中定义NF_ACCEPT这种动作,对应常数为1*/ 当用户在iptables中输入是"ACCEPT"字符串时,将其转换成: verdict=-NF_ACCEPT - 1 到了内核中,要用到NETFILTER的动作时,只需要反过来: (unsigned)(-verdict) - 1; 就OK了。 也就是说,用: v=target.verdict; if(v<0) /*内核默认动作*/ { if (v != IPT_RETURN) { return verdict = (unsigned)(-v) - 1; /*是默认动作,且不为RETURN,直接返回*/ } //以下代码处理RETURN的情况 …… } else /*自定义链*/ { } 就可以处理内建动作与自定义链或RETURN几种情况了。让我们来看内核中实际的这块的代码处理: /*获取当前规则的target*/ t = ipt_get_target(e); /*如果不存在target模块函数,那么target应为常数,如ACCEPT,DROP等,或是自定义链*/ if (!t->u.kernel.target->target) { int v; v = ((struct ipt_standard_target *)t)->verdict; /*取得target的verdict值*/ /* #define NF_DROP 0 #define NF_ACCEPT 1 #define NF_STOLEN 2 #define NF_QUEUE 3 #define NF_REPEAT 4 #define NF_MAX_VERDICT NF_REPEAT #define IPT_CONTINUE 0xFFFFFFFF #define IPT_RETURN (-NF_MAX_VERDICT - 1) */ if (v < 0) /*动作是默认内建的动作*/ { /* Pop from stack? */ if (v != IPT_RETURN) /*如果不是Return,返回相应的动作*/ { verdict = (unsigned)(-v) - 1; break; } /*back用来描述return 动作,或者是自定义链执行完了,若还需继续匹配,那它指向那条应继续匹配的规则,所以,这里用e=back来取得下一条待匹配的规则*/ e = back; /*重新设置back点*/ back = get_entry(table_base, back->comefrom); continue; } /*v>=0的情况,v表示了一个偏移值——待跳转的自定义链相对于规则起始地址的偏移,即如果是自定义链,那么应该跳到哪条规则去继续执行匹配, 这里这个判断的意思是,如果下一条跳转换规则刚好就是当前规则的下一条规则,那就不用跳了……,否则,将当前规则(形如-j 自定义链名这样的规则)的下一条规则设置为back点*/ if (table_base + v!= (void *)e + e->next_offset) /*当前链后还有规则*/ { /* Save old back ptr in next entry */ struct ipt_entry *next = (void *)e + e->next_offset; next->comefrom = (void *)back - table_base; /* set back pointer to next entry */ back = next; } /*确定等匹配的下一条规则*/ e = get_entry(table_base, v); } else /*如果存在target模块函数*/ { /* 如果需要继续处理则返回IPT_CONTINUE(-1),否则返回NF_ACCEPT、NF_DROP等值 */ verdict = t->u.kernel.target->target(pskb, hook, in, out, t->data, userdata); /* Target might have changed stuff. */ /*Target函数有可能已经改变了stuff,所以这里重新定位指针*/ ip = (*pskb)->nh.iph; protohdr = (u_int32_t *)ip + ip->ihl; datalen = (*pskb)->len - ip->ihl * 4; /*如果返回的动作是继续检查下一条规则,则设置当前规则为下一条规则,继续循环,否则, 就跳出循环,因为在ipt_do_table函数末尾有return verdict;表明,则将target函数决定的返回值返回给 调用函数nf_iterate,由它来根据verdict决定数据包的命运*/ if (verdict == IPT_CONTINUE) e = (void *)e + e->next_offset; else /* Verdict */ break; }
