本人研究linux的防火墙系统也有一段时间了,由于近来涉及到的工作比较纷杂,久而久之怕生熟了。趁有时间,好好把这方面的东西总结一番。一来是给自己做个沉淀,二来也欢迎这方面比较牛的前辈给小弟予以指点,共同学习,共同进步。
能在CU上混的人绝非等闲之辈。因此,小弟这里说明一下:本系列博文主要侧重于分析Netfilter的实现机制,原理和设计思想层面的东西,同时从用户态的iptables到内核态的Netfilter其交互过程和通信手段等。至于iptables的入门用法方面的东西,网上随便一搜罗就有一大堆,我这里不浪费笔墨了。
很多人在接触iptables之后就会这么一种感觉:我通过iptables命令配下去的每一条规则,到底是如何生效的呢?内核又是怎么去执行这些规则匹配呢?如果iptables不能满足我当下的需求,那么我是否可以去对其进行扩展呢?这些问题,都是我在接下来的博文中一一和大家分享的话题。这里需要指出:因为Netfilter与IP协议栈是无缝契合的,所以如果你要是有协议栈方面的基础,在阅读本文时一定会感觉轻车熟路。当然,如果没有也没关系,因为我会在关键点就协议栈的入门知识给大家做个普及。只是普及哦,不会详细深入下去的,因为涉及的东西太多了,目前我还正在研究摸索当中呢。好了,废话不多说,进入正题。
备注:我研究的内核版本是2.6.21,iptables的版本1.4.0。
什么是Netfilter?
为了说明这个问题,首先看一个网络通信的基本模型:
在数据的发送过程中,从上至下依次是“加头”的过程,每到达一层数据就被会加上该层的头部;与此同时,接受数据方就是个“剥头”的过程,从网卡收上包来之后,在往协议栈的上层传递过程中依次剥去每层的头部,最终到达用户那儿的就是裸数据了。
那么,“栈”模式底层机制基本就是像下面这个样子:
对于发送的每个数据包,首先也有一个路由判决,以确定该包是从哪个接口出去,然后经过“D”点,最后也是顺着“E”点将该包发送出去。
协议栈那五个关键点A,B,C,D和E就是我们Netfilter大展拳脚的地方了。
Netfilter是Linux 2.4.x引入的一个子系统,它作为一个通用的、抽象的框架,提供一整套的hook函数的管理机制,使得诸如数据包过滤、网络地址转换(NAT)和基于协议类型的连接跟踪成为了可能。Netfilter在内核中位置如下图所示:
这幅图,很直观的反应了用户空间的iptables和内核空间的基于Netfilter的ip_tables模块之间的关系和其通讯方式,以及Netfilter在这其中所扮演的角色。
回到前面讨论的关于协议栈那五个关键点“ABCDE”上来。Netfilter在netfilter_ipv4.h中将这个五个点重新命了个名,如下图所示,意思我就不再解释了,猫叫咪咪而已:
在每个关键点上,有很多已经按照优先级预先注册了的回调函数(后面再说这些函数是什么,干什么用的。有些人喜欢把这些函数称为“钩子函数”,说的是同一个东西)埋伏在这些关键点,形成了一条链。对于每个到来的数据包会依次被那些回调函数“调戏”一番再视情况是将其放行,丢弃还是怎么滴。但是无论如何,这些回调函数最后必须向Netfilter报告一下该数据包的死活情况,因为毕竟每个数据包都是Netfilter从人家协议栈那儿借调过来给兄弟们Happy的,别个再怎么滴也总得“活要见人,死要见尸”吧。每个钩子函数最后必须向Netfilter框架返回下列几个值其中之一:
- NF_ACCEPT 继续正常传输数据报。这个返回值告诉 Netfilter:到目前为止,该数据包还是被接受的并且该数据包应当被递交到网络协议栈的下一个阶段。
- NF_DROP 丢弃该数据报,不再传输。
- NF_STOLEN 模块接管该数据报,告诉Netfilter“忘掉”该数据报。该回调函数将从此开始对数据包的处理,并且Netfilter应当放弃对该数据包做任何的处理。但是,这并不意味着该数据包的资源已经被释放。这个数据包以及它独自的sk_buff数据结构仍然有效,只是回调函数从Netfilter 获取了该数据包的所有权。
- NF_QUEUE 对该数据报进行排队(通常用于将数据报给用户空间的进程进行处理)
- NF_REPEAT 再次调用该回调函数,应当谨慎使用这个值,以免造成死循环。
为了让我们显得更专业些,我们开始做些约定:上面提到的五个关键点后面我们就叫它们为hook点,每个hook点所注册的那些回调函数都将其称为hook函数。
对于每种类型的协议,数据包都会依次按照hook点的方向进行传输,每个hook点上Netfilter又按照优先级挂了很多hook函数。这些hook函数就是用来处理数据包用的。
Netfilter使用NF_HOOK(include/linux/netfilter.h)宏在协议栈内部切入到Netfilter框架中。相比于2.4版本,2.6版内核在该宏的定义上显得更加灵活一些,定义如下:
#define NF_HOOK(pf, hook, skb, indev, outdev, okfn) \
NF_HOOK_THRESH(pf, hook, skb, indev, outdev, okfn, INT_MIN)
关于宏NF_HOOK各个参数的解释说明:
- pf:协议族名,Netfilter架构同样可以用于IP层之外,因此这个变量还可以有诸如PF_INET6,PF_DECnet等名字。
- hook:HOOK点的名字,对于IP层,就是取上面的五个值;
- skb:不解释;
- indev:数据包进来的设备,以struct net_device结构表示;
- outdev:数据包出去的设备,以struct net_device结构表示;
(后面可以看到,以上五个参数将传递给nf_register_hook中注册的处理函数。) - okfn:是个函数指针,当所有的该HOOK点的所有登记函数调用完后,转而走此流程。
而NF_HOOK_THRESH又是一个宏:
#define NF_HOOK_THRESH(pf, hook, skb, indev, outdev, okfn, thresh) \
({int __ret; \
if ((__ret=nf_hook_thresh(pf, hook, &(skb), indev, outdev, okfn, thresh, 1)) == 1)\
__ret = (okfn)(skb); \
__ret;})
我们发现NF_HOOK_THRESH宏只增加了一个thresh参数,这个参数就是用来指定通过该宏去遍历钩子函数时的优先级,同时,该宏内部又调用了nf_hook_thresh函数:
static inline int nf_hook_thresh(int pf, unsigned int hook,
struct sk_buff **pskb,
struct net_device *indev,
struct net_device *outdev,
int (*okfn)(struct sk_buff *), int thresh,
int cond)
{
if (!cond)
return 1;
#ifndef CONFIG_NETFILTER_DEBUG
if (list_