思想
标准IP路由查找的过程为我们提供了一个极好的“匹配-动作”的例程。即匹配到一个路由项,然后将数据包发给该路由项指示的下一跳。
struct result_node { uint32 network; uint32 netmask; void *action; };以上这个思想多亏了路由查找中的“最长前缀匹配”原则,该原则是隐式的,但是该原则保证了最精确的匹配。
需求 在我的特殊场景中,我希望一个网络段(一个大的网络,一个小一点的网络,一台主机)发出的数据流和一个字符串描述关联起来,该字符串可以是描述,可以是用户名,它甚至可以是别的任意什么东西...以往这种情况会被认为是不符合UNIX哲学的,并且在以往,内存使用方式太小气,内存太奢侈。但是现如今,不需要吝啬内存了,我们便可以在内核里面塞入任何可以塞入的东西,只要设计得当,让它符合UNIX哲学精神即可。
为何Linux没有实现 实际上我的想法一开始就是错误的。正确性在于我知道我是错的。为何一直以来我一直在“修补”Linux内核协议栈以及Netfilter扩展的各种不良或者不完备的实现呢,比如“立即生效NAT”,比如双向静态NAT,比如不完备的conntrack confirm机制,不一而足。这些缺陷难道Linux内核以及Netfilter社区的那帮大牛们意识不到吗?绝对不是这样,因为他们遵循的是Worse is better原则,该原则的核心就是简单主宰一切,为了简单可以舍弃该舍弃的一切。
Simplicity is the most important consideration in a design.CompletenessThe design must cover as many important situations as is practical. All reasonably expected cases should be covered. Completeness can be sacrificed in favor of any other quality. In fact, completeness must be sacrificed whenever implementation simplicity is jeopardized. Consistency can be sacrificed to achieve completeness if simplicity is retained; especially worthless is consistency of interface.所以,Linux的社区开发人员严格遵循了该原则,没有引入“复杂且不常用”的机制。而我为何非要实现它们呢?我的想法是一切为了满足我的需求,依照Simplicity优先原则,我不得不这么做,因为我这么做是为了不引入更复杂的机制。
分析
既然是给数据流绑定一个字符串,很显然,扩展nf_conntrack是绝佳的选择,如何扩展它我在前文已经做了详述。接下来需要考虑的是如何设置规则,很显然,写一个INFO iptables模块是一个选择:
iptables -t ...-A .... -j INFO --set-info "aaaaaaaaaaaa"
iptables ... -m state --state ESTABLISHED -j ACCEPT (设置在第一条,因为只有针对一个流的第一个NEW状态的数据包才会有set-info的必要)
1.当INFO和INFO之外的其它target并不一致同意通过以上的方式跳出规则匹配链的时候,就需要安排另一条自定义链来解决。
UNIX思想教导我们要将问题拆成互相独立的小问题,然后让它们相互配合去解决最终的大问题,但是相互配合本身有时会成为新的问题,需要付出巨大的管理成本。并不是每一类问题都可以拆成cat file|grep key那样的方式的。
1.使用路由查找模块是完全独立于iptables,和iptables没有半毛钱关系;
再次扩展 话说可以使用路由查找模块快速匹配到一个数据包关联的一个“路由项”,那么该路由项里面仅仅存一个字符串信息是不是太浪费了呢?能不能把路由项携带的信息也搞成可扩展的呢?答案无疑是肯定的,因为我已经将“下一跳”修改成了字符串信息,接下来就是取消这个类型定义就可以了,在路由项里面加一个void *指针无疑是可取的,但是为了使内存更紧凑,加入一个0长度数组是更好的选择。
/* * 以下结构体指示一个“路由节点”,它可以携带一个extra_data * 你可以任意定义它,比如可以如下定义: * struct my_extra { * char info[INFO_SIZE]; * int policy; // NF_ACCEPT,NF_DROP或者其它? * // .... extra of extra ?? * }; * 它带来了无限的扩展性,你可以使用这个“路由”节点存储任何数据。 */ struct nf_fib_node { struct hlist_node nf_fn_hash; u_int32_t fn_key; int len; // extra_len指示你传入的extra data的长度 int extra_len; // 0长度数组,你可以任意定义它 char extra[0]; };
PS:《JAVA编程思想》第15章,15.16小节一开始:介绍过这样的思想,即要编写能够尽可能广泛地应用的代码。为了实现这一点,我们需要各种途径来放松对我们的代码将要作用的类型所作的限制,同时不丢失静态类型检查的好处。
进一步扩展 ACL的全称为访问控制列表,注意“列表”这个词,表明它的结构是一维线性。在很多的系统上,ACL对数据包的具体匹配行为依赖管理员的规则配置顺序。注意以上说的是ACL的用户配置接口,当然对于这一点,Linux的iptables以及Cisco的ACL是一致的,但是对于内部的实现,各家系统就有所区别了。以Linux的iptables为例,考虑以下的规则序列:
i=1; # 添加巨量的iptables规则 for ((; i < 255; i++)); do j=1; for ((; j < 255; j++)); do iptables -A FORWARD -s 192.168.$i.$j -j DROP; done done
struct my_extra { // 替换iptables中的matches struct list_head matches_list; // 替换iptables的target int policy; // NF_ACCEPT,NF_DROP或者其它? };
实现 上述的“进一步扩展”一节中说明的思想仅仅指出了一种可行性,我还没有想出把它放在哪个位置好。写下本文的目的最初是为了实现一个叫做conntrack_table的工具,针对nf_conntrack来做操作,比如:
1.在conntrack里面绑定两个方向的路由,实现仅仅针对conntrack进行查找,不再针对每一个数据包都去查路由表;
内核机制实现思路 内核态一共需要实现两个部分,第一个部分是将标准的路由查找算法复制移植一份到nf_conntrack_ipv4模块中,第二部分就是在nf_conntrack被confirm的地方查询这个路由表,取出结果路由项中保存的info信息存储在conntrack中,这个操作仅仅针对一个流的第一个数据包。后续的数据包不再查询这个路由表。内核的路由查找模块的移植代码一会儿我再给出,现在先给出针对confirm的修改,修改的仅有ipv4_confirm函数,它是挂接于INET IPv4协议族的一个HOOK函数,这样我便不必考虑其它的协议了,从而省略了协议判断:
static unsigned int ipv4_confirm(unsigned int hooknum, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *)) { ... out: /* We've seen it coming out the other side: confirm it */ ret = nf_conntrack_confirm(skb); if (ret == NF_ACCEPT) { #include "nf_conntrack_info.h" #define MAX_LEN 128 struct nf_conn_priv { struct nf_conn_counter ncc[IP_CT_DIR_MAX]; char *info; }; struct nf_conn_priv *ncp; struct nf_conn_counter *acct; acct = nf_conn_acct_find(ct); if (acct) { char buf[MAX_LEN] = {0}; int len = MAX_LEN; struct iphdr *iph = ip_hdr(skb); // 查询“路由表”,获取结果 int rv = nf_route_table_search(iph->saddr, buf, &len); if (!rv) { ncp = (struct nf_conn_priv *)acct; if (ncp->info == NULL) { ncp->info = (char *)kcalloc(1, len+1, GFP_ATOMIC); } // 拷贝获取的结果到conntrack memcpy(ncp->info, buf, len); } } } return ret; }
头文件:nf_conntrack_rtable.h
#include <linux/types.h> #include <linux/list.h> #define SIZE 128 struct nf_fn_zone { struct nf_fn_zone *fz_next; /* Next not empty zone */ struct hlist_head *fz_hash; /* Hash table pointer */ int fz_nent; /* Number of entries */ int fz_divisor; /* Hash divisor */ u32 fz_hashmask; /* (fz_divisor - 1) */ #define FZ_HASHMASK(fz) ((fz)->fz_hashmask) int fz_order; /* Zone order */ u_int32_t fz_mask; #define FZ_MASK(fz) ((fz)->fz_mask) }; struct nf_fn_hash { struct nf_fn_zone *nf_fn_zones[33]; struct nf_fn_zone *nf_fn_zone_list; }; /* * 以下结构体指示一个“路由节点”,它可以携带一个extra_data * 你可以任意定义它,比如可以如下定义: * struct my_extra { * char info[128]; * int policy; // NF_ACCEPT,NF_DROP或者其它? * // .... extra of extra ?? * }; * 它带来了无限的扩展性,你可以使用这个“路由”节点存储任何数据。 */ struct nf_fib_node { struct hlist_node nf_fn_hash; u_int32_t fn_key; int len; // extra_len指示你传入的extra data的长度 int extra_len; // 0长度数组,你可以任意定义它 char extra[0]; }; // 查找接口 int nf_route_table_search(const u_int32_t dst, void *res, int *res_len); // 添加接口 int nf_route_table_add( u_int32_t network, u_int32_t netmask, void *extra, int extra_len); // 节点删除接口 int nf_route_table_delete(u_int32_t network, u_int32_t mask); // 清除接口 void nf_route_table_clear(void);
C文件:nf_conntrack_rtable.c
#include <linux/types.h> #include <linux/inetdevice.h> #include <linux/slab.h> #include <linux/kernel.h> #include "nf_conntrack_info.h" #ifndef NULL #define NULL 0 #endif // 使用lock总是安全的。内核编程两要素:1.安全;2.高效 static DEFINE_RWLOCK(nf_hash_lock); struct nf_fn_hash *route_table = NULL; static inline u_int32_t nf_fz_key(u_int32_t dst, struct nf_fn_zone *fz) { return dst & FZ_MASK(fz); } static inline u32 nf_fn_hash(u_int32_t key, struct nf_fn_zone *fz) { u32 h = key>>(32 - fz->fz_order); h ^= (h>>20); h ^= (h>>10); h ^= (h>>5); h &= FZ_HASHMASK(fz); return h; } static struct hlist_head *fz_hash_alloc(int divisor) { unsigned long size = divisor * sizeof(struct hlist_head); return kcalloc(1, size, GFP_ATOMIC); } static struct nf_fn_zone * fn_new_zone(struct nf_fn_hash *table, int z) { int i; struct nf_fn_zone *fz = kcalloc(1, sizeof(struct nf_fn_zone), GFP_ATOMIC); if (!fz) return NULL; if (z) { fz->fz_divisor = 16; } else { fz->fz_divisor = 1; } fz->fz_hashmask = (fz->fz_divisor - 1); fz->fz_hash = fz_hash_alloc(fz->fz_divisor); if (!fz->fz_hash) { kfree(fz); return NULL; } fz->fz_order = z; fz->fz_mask = inet_make_mask(z); /* Find the first not empty zone with more specific mask */ for (i=z+1; i<=32; i++) if (table->nf_fn_zones[i]) break; write_lock_bh(&nf_hash_lock); if (i>32) { /* No more specific masks, we are the first. */ fz->fz_next = table->nf_fn_zone_list; table->nf_fn_zone_list = fz; } else { fz->fz_next = table->nf_fn_zones[i]->fz_next; table->nf_fn_zones[i]->fz_next = fz; } table->nf_fn_zones[z] = fz; write_unlock_bh(&nf_hash_lock); return fz; } // 路由表操作接口:1.查找;2.删除。参数过于多,类似Win32 API,风格不好,但使用方便 int nf_route_table_opt(const u_int32_t dst, const u_int32_t mask, int del_option, void *res, int *res_len) { int rv = 1; struct nf_fn_zone *fz; struct nf_fib_node *del_node = NULL; if (NULL == route_table) { printk(""); return 1; } read_lock(&nf_hash_lock); for (fz = route_table->nf_fn_zone_list; fz; fz = fz->fz_next) { struct hlist_head *head; struct hlist_node *node; struct nf_fib_node *f; u_int32_t k = nf_fz_key(dst, fz); head = &fz->fz_hash[nf_fn_hash(k, fz)]; hlist_for_each_entry(f, node, head, nf_fn_hash) { if (f->fn_key == k){ if ( 1 == del_option && mask == FZ_MASK(fz)){ del_node = f; } else if (0 == del_option){ // 将用户传入的extra数据拷贝给调用者。 memcpy(res, (const void *)(f->extra), f->extra_len); *res_len = f->extra_len; } rv=0; goto out; } } } rv = 1; out: read_lock(&nf_hash_lock); if (del_node) { write_lock_bh(&nf_hash_lock); __hlist_del(&del_node->nf_fn_hash); kfree(del_node); write_unlock_bh(&nf_hash_lock); } return rv; } static inline void fib_insert_node(struct nf_fn_zone *fz, struct nf_fib_node *f) { struct hlist_head *head = &fz->fz_hash[nf_fn_hash(f->fn_key, fz)]; hlist_add_head(&f->nf_fn_hash, head); } int nf_route_table_search(u_int32_t dst, void *res, int *res_len) { return nf_route_table_opt(dst, 32, 0, res, res_len); } int nf_route_table_delete(u_int32_t network, u_int32_t mask) { return nf_route_table_opt(network, mask, 1, NULL, NULL); } int nf_route_table_add(u_int32_t network, u_int32_t netmask, void *extra, int extra_len) { struct nf_fib_node *new_f; struct nf_fn_zone *fz; new_f = kcalloc(1, sizeof(struct nf_fib_node) + extra_len, GFP_ATOMIC); new_f->len = inet_mask_len(netmask); new_f->extra_len = extra_len; new_f->fn_key = network; memcpy(new_f->extra, extra, extra_len); if (new_f->len > 32) { return -1; } INIT_HLIST_NODE(&new_f->nf_fn_hash); if ( NULL == route_table){ route_table = kcalloc(1, sizeof(struct nf_fn_hash), GFP_ATOMIC); fz = fn_new_zone(route_table,new_f->len); } else { fz = route_table->nf_fn_zones[new_f->len]; } if (!fz && !(fz = fn_new_zone(route_table,new_f->len))) { return -1; } fib_insert_node(fz, new_f); return 0; } void nf_route_table_clear( void ) { struct nf_fn_zone *fz,*old; if (NULL == route_table) { printk(""); return; } write_lock_bh(&nf_hash_lock); for (fz = route_table->nf_fn_zone_list; fz; ) { if (fz != NULL){ kfree(fz->fz_hash); fz->fz_hash=NULL; old = fz; fz = fz->fz_next; kfree(old); old=NULL; } } kfree(route_table); route_table=NULL; write_unlock_bh(&nf_hash_lock); return; }
nf_conntrack_ipv4-objs := nf_conntrack_l3proto_ipv4.o nf_conntrack_proto_icmp.o nf_conntrack_info.o然后编译即可,出来的nf_conntrack_ipv4.ko就已经加入自己的扩展了。
用户接口 在抛弃iptables之后,我选择了procfs作为用户接口,因为它使用标准的文件IO接口,可以方便的在各种脚本中操作。这里还有三个思想。
echo +192.168.10.0 255.255.255.0 1234abcd >/proc/nfrtable
关于算法的重用 Linux内核是不拘一格的,其算法是公开的,因此也就没有什么Copy right一说,所以任何人都可以使用。关键的一点是,移植它的算法需要付出一点代价,正如Linus所说的那句经典的RTFS,Linux源码
不提供任何的可移植性接口供用户使用,一切都需要你自己来做!比如,你不能在编译内核的时候将一些可重用的模块编译成一个Library,如果你需要,你必须自己来修改Makefile。
关于Linux
内核API Linux内核API是多变的,这种运动给它带来的最大好处在于灵活,还是那句话,RTFS。源码就在你眼前,看到有需要的自己拿去,不过内核社区不提供任何搬运支持。正如一座金矿,管理者只是在管理它,任何淘金者都可以挖走自己需要的金子,但是管理者不提供任何车辆负责搬运,即使你有钱想租也没有,要知道,金子是很重的哦!(免责声明:文章内容如涉及作品内容、版权和其它问题,请及时与我们联系,我们将在第一时间删除内容,文章内容仅供参考)