为梦而战
全部博文(185)
分类: LINUX
2014-10-16 09:31:43
Linux 的防火墙技术经历了若干代的沿革,一步步的发展而来。最开始的 ipfwadm 是 Alan Cox 在 Linux kernel 发展的初期,从 FreeBSD 的内核代码中移植过来的。后来经历了 ipchains,再经由 Paul Russell 在 Linux kernel 2.3 系列的开发过程中发展了 netfilter 这个架构。而用户空间的防火墙管理工具,也相应的发展为 iptables。netfilter/iptables 这个组合目前相当的令人满意。在经历了 Linux kernel 2.4 和 2.5 的发展以后,的确可以说,netfilter/iptables 经受住了大量用户广泛使用的考验。
本文并不打算介绍 Linux 防火墙在用户空间的管理程序 iptables 的使用。至于如何利用 netfilter/iptables 机制搭建一个可靠的 Internet 防火墙,这也不是本文感兴趣的话题。关于 iptables 的使用,读者朋友们可以参考 man iptables 的手册,也可以参考 netfilter 的核心开发者 Paul Russell 写的 Packet Filtering HOW-TO 和 NAT HOW-TO。相关的链接,请参见文后所列的参考资料目录。读者朋友们在阅读本文之前,最好能够对 iptables 的使用有一定的了解。
本文介绍 netfilter 在 Linux kernel 中的实现。如果条件允许的话,我们可能在后续的文章中将要进一步说明如何编写自己的 kernel modules 并将其镶嵌在 netfilter 的架构中,以实现自己的定制防火墙功能。值得指出的是,在 netfilter 的网站上,可以看到 netfilter 的一个子项目 patch-o-matic,其中收录了大量的各种定制 kernel modules,这些 modules 给读者朋友们开发自己的 kernel modules,提供了非常多的、很好的例子。
netfilter 在 Linux kernel 中的 IPv4、IPv6 和 DECnet 等网络协议栈中都有相应的实现。本文限于篇幅,将只介绍其中最让大多数读者朋友们感兴趣的 IPv4 协议栈上的 netfilter 的实现。
我们在编译 Linux kernel 的过程中一定会注意到,netfilter 是一个在编译过程中可选的部件。也就是说,用户在编译内核的过程中,可以按照自己的需要,决定是否要在自己的内核中编译进去 netfilter 的 kernel 支持。这就带给我们一个提示,实现 netfilter 的代码对于实现 IPv4 协议栈的代码的影响应该会是尽量的小,不那么引人注目才对。否则的话,IPv4 协议栈的代码维护工作就不得不和实现 netfilter 的代码的维护工作搅在一起,让人头疼了。
事实也的确如此,IPv4 协议栈为了实现对 netfilter 架构的支持,在 IP packet 在 IPv4 协议栈上的游历路线之中,仔细选择了五个参考点。在这五个参考点上,各引入了一行对 NF_HOOK() 宏函数的一个相应的调用。这五个参考点被分别命名为 PREROUTING,LOCAL-IN,FORWARD,LOCAL-OUT 和 POSTROUTING。关于这五个参考点的含义,在 iptables 的使用说明中有准确的叙述,相信读者朋友们都应该了解了。从如下的 grep 输出,我们可以看到 IPv4 协议栈实现代码对 NF_HOOK() 宏函数的调用:
zhaoway@qhq ~/linux-2.4.19/net/ipv4 $ grep -n NF_HOOK *.c arp.c:591:NF_HOOK(NF_ARP, NF_ARP_OUT, skb, NULL, dev, dev_queue_xmit); arp.c:871:return NF_HOOK(NF_ARP, NF_ARP_IN, skb, dev, NULL, arp_process); igmp.c:187:/* Don't just hand NF_HOOK skb->dst->output, in case netfilter hook igmp.c:252:return NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, rt->u.dst.dev, ip_forward.c:145:return NF_HOOK(PF_INET, NF_IP_FORWARD, skb, skb->dev, dev2, ip_gre.c:668:/* Need this wrapper because NF_HOOK takes the function address */ ip_input.c:302:return NF_HOOK(PF_INET, NF_IP_LOCAL_IN, skb, skb->dev, NULL, ip_input.c:437:return NF_HOOK(PF_INET, NF_IP_PRE_ROUTING, skb, dev, NULL, ip_output.c:111:/* Don't just hand NF_HOOK skb->dst->output, in case netfilter hook ip_output.c:156:return NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, rt->u.dst.dev, ip_output.c:191:return NF_HOOK(PF_INET, NF_IP_POST_ROUTING, skb, NULL, dev, ip_output.c:233:NF_HOOK(PF_INET, NF_IP_POST_ROUTING, newskb, NULL, ip_output.c:249:NF_HOOK(PF_INET, NF_IP_POST_ROUTING, newskb, NULL, ip_output.c:400:return NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, rt->u.dst.dev, ip_output.c:603:err = NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, ip_output.c:714:err = NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, rt->u.dst.dev, ipip.c:516:/* Need this wrapper because NF_HOOK takes the function address */ ipmr.c:1211:NF_HOOK(PF_INET, NF_IP_FORWARD, skb2, skb->dev, dev, zhaoway@qhq ~/linux-2.4.19/net/ipv4 $
NF_HOOK() 这个宏函数,定义在 linux-2.4.19/include/linux/netfilter.h 里面。当 #ifdef CONFIG_NETFILTER 被定义的时候,就转去调用 nf_hook_slow() 函数;如果 CONFIG_NETFILTER 没有被定义,则从 netfilter 模块转回到 IPv4 协议栈,继续往下处理。这样就给了用户在编译 kernel 的时候一个选项,可以通过定义 CONFIG_NETFILTER 与否来决定是否把 netfilter 支持代码编译进内核。从这个函数的名称,我们也可以猜到,可以把 IPv4 协议栈上的这五个参考点,形象的看成是五个钩子。IP packet 在 IPv4 协议栈上游历的时候,途经这五个钩子,就会被 netfilter 模块钓上来,审查一番,并据审查的结果,决定 packet 的下一步命运:是被原封不动的放回 IPv4 协议栈,继续游历;还是经过一些修改,再放回去;还是干脆丢弃掉算了?
IP packet 被 NF_HOOK() 从 IPv4 协议栈上钓出来以后,就进入 linux-2.4.19/net/core/netfilter.c 中的 nf_hook_slow() 函数进行处理。这个函数干的主要事情,就是根据 nf_hooks[] 数组,开始处理 packet。准确地说来,上一段讲到的 IPv4 协议栈上的五个参考点,并不是“钓鱼的钩子”,而是“允许垂钓的地点”。换句话说,IPv4 协议栈上定义了五个“允许垂钓点”。在每一个“垂钓点”,都可以让 netfilter 放置一个“鱼钩”,把经过的 packet 钓上来。那么 netfiler 的“鱼钩”都放在什么地方?就放在 nf_hooks[][] 数组里面。这个“鱼钩”用 linux-2.4.19/include/linux/netfilter.h 中定义的如下 struct 予以描述:
struct nf_hook_ops { struct list_head list; nf_hookfn *hook; int pf; int hooknum; int priority; };
我们看到,“鱼钩”的本质,是一个 nf_hookfn 函数。这个函数将对被钓上来的 IP packet 进行初步的处理。那么,这些“鱼钩”是由谁来放置到 nf_hooks[][] 数组里面的呢?答案是,各个 table。熟悉 iptables 管理工具的读者朋友们应该了解,一个 table 就是一组类似的防火墙 rules 的集合。iptables 里面默认定义了三个 table:filter,mangle,和 nat。举 filter table 为例,它是在 linux-2.4.19/net/ipv4/netfilter/iptable_filter.c 中实现的一个 kernel module。在这个 module 的初始化过程中,它会调用 nf_register_hook() 向 netfilter 的核心代码注册一组“鱼钩”。这个注册过程,实际上,也就是把“鱼钩”放到“垂钓点”的过程。“垂钓点”的具体位置,由 nf_hooks[][] 数组的下标具体说明。
我们具体看到 linux-2.4.19/net/ipv4/netfilter/iptable_filter.c 也就是 filter table 的实现代码,就发现 filter table 中的“鱼钩”上的 nf_hookfn 函数,主要是在调用 ipt_do_table() 函数。这是一个定义在 linux-2.4.19/net/ipv4/netfilter/ip_tables.c 中的函数。前面提到过,一个 table 就是一组防火墙 rules 的集合。显然,ipt_do_table() 函数将要做的事情,就是按照 table 中存储的一条又一条的 rules 来处理被“钓”上来的 IP packet。
table 里面存放了这个 table 中所有的防火墙 rules。但是并不是所有的 rules 都要拿过来,按照它审查一下这个 packet。事实上,这个 packet 是从哪个“鱼钩”上被钓上来的,就只有和那个“鱼钩”相关的 rules 才被拿过来,用来审查这个 packet。这个机制,就为每个 table 实现了多个 chain,而每个 chain 上又有多个 rules。而且,我们立刻看到,一个 chain 是和 IPv4 协议栈上的一个“垂钓点”相对应的。熟悉 iptables 用户空间管理工具的使用的读者朋友们应该立刻就会注意到这一点了。
在 linux-2.4.19/include/linux/netfilter_ipv4/ip_tables.h 中定义了 table 中的 rule 的存放格式,如下:
/* This structure defines each of the firewall rules. Consists of 3 parts which are 1) general IP header stuff 2) match specific stuff 3) the target to perform if the rule matches */ struct ipt_entry { struct ipt_ip ip; /* Mark with fields that we care about. */ unsigned int nfcache; /* Size of ipt_entry + matches */ u_int16_t target_offset; /* Size of ipt_entry + matches + target */ u_int16_t next_offset; /* Back pointer */ unsigned int comefrom; /* Packet and byte counters. */ struct ipt_counters counters; /* The matches (if any), then the target. */ unsigned char elems[0]; };
一个 entry 就是一个 rule。一个 entry 主要由两部分组成。一部分,是一系列的 matches;另一部分,是一个 target。这若干个 match 所要回答的问题,是相关的 packet 和本条 rule 是否匹配。而这个 target 所要回答的问题,是一旦 packet 匹配上以后,该拿这个 packet 怎么办?也就是要由 target 来决定这个匹配的 packet 今后的命运了。开头的 struct ipt_ip 的定义如下:
struct ipt_ip { /* Source and destination IP addr */ struct in_addr src, dst; /* Mask for src and dest IP addr */ struct in_addr smsk, dmsk; char iniface[IFNAMSIZ], outiface[IFNAMSIZ]; unsigned char iniface_mask[IFNAMSIZ], outiface_mask[IFNAMSIZ]; /* Protocol, 0 = ANY */ u_int16_t proto; /* Flags word */ u_int8_t flags; /* Inverse flags */ u_int8_t invflags; };
我们立刻可以看出来,在 struct ipt_ip 里面记录了关于这个 rule 所要匹配(match)的 packet 的一些特征。
netfilter 核心部分提供了一个分析、处置 packet 的架构,但是核心部分代码并不具体的去分析、处置 packet。这个具体的分析、处置的任务被交给其它的 module 来完成。核心部分代码可以根据 table 中记录的 rules 信息,来把 packet 交给能够处理相应的 rules 的 module 代码。那么,核心代码如何了解哪一个 module 可以处理哪一类的 rules 的呢?这要由各个相应的 modules 起动的时候,主动去向核心代码注册,ipt_register_target() 或者是 ipt_register_match()。这个注册过程,主要就是通知核心代码,本 module 有一个 target() 函数,可以决定 packet 的命运;或者是,本 module 有一个 match() 函数,可以判定一个 packet 是否符合 rules 的匹配要求。
这就提示我们,如果要写自己的防火墙模块,镶嵌在 netfilter 的架构中的话,我们主要要做的任务,就是向 netfilter 核心注册 ipt_register_target() 或者 ipt_register_match()。
最后,要说明的是 iptables,这个位于用户空间的管理工具。前面我们看到了,netfilter 在内核空间的代码根据 table 中的 rules,完成对 packet 的分析和处置。但是这些 table 中的具体的防火墙 rules,还是必须由系统管理员亲自编写。kernel 中的 netfilter 只是提供了一个机制,它并不知道该怎样利用这个机制,写出合适的 rules,来实现一个网络防火墙。那么,系统管理员编写的 rules,怎样进入位于 kernel 空间中的 netfilter 维护的 table 中去呢?
这个任务是由 iptables 这个工具来完成的。它经过 getsockopt() 以及 setsockopt() 两个系统调用,进入 kernel 空间。这两个调用是 BSD Socket 接口的一部分。这里面的问题是 IPv4 在接到关于某个 sock 的不认识的 opt 的时候,应该怎么处理?netfilter 要求它在 linux-2.4.19/net/ipv4/ip_sockglue.c 文件中处理 getsockopt() 和 setsockopt() 系统调用的 ip_sockopt() 函数中适当的地方调用 nf_sockopt()。这样,用户空间就可以和 netfilter 核心部分进行交流,可以维护 table 中的防火墙 rules 了。
netfilter 对于 IPv4 的修改非常小,一是在若干个地方调用了 NF_HOOK(),二是在 ip_sockopt() 中调用了 nf_sockopt()。netfilter 的核心部分代码只是维护 table,解释 table 的任务在于其它的 kernel module。netfilter 会把从 hook “钓”起来的 packet 以及 table 里面的相关内容发给注册了的 module,决定 packet 的命运。