一、基本介绍
1.1 Netfilter概述
Netfilter/IPTables是Linux2.4.x之后新一代的Linux防火墙机制,是linux内核的一个子系统。Netfilter采用模块化设计,具有良好的可扩充性。其重要工具模块IPTables从用户态的iptables连接到内核态的Netfilter的架构中,Netfilter与IP协议栈是无缝契合的,并允许使用者对数据报进行过滤、地址转换、处理等操作。
1.2 基本名词解释
1、target//规则匹配后的处理方法
一般将target分为两类,一类为标准的target,即下面的宏定义
NF_DROP
NF_ACCEPT
NF_STOLEN
NF_QUEUE
NF_REPEAT
另一类为由模块扩展的target,
REJECT,LOG,ULOG,TOS,DSCP,MARK,REDIRECT,MASQUERADE,NETMAP
2、hook
//这个成员用于指定安装的这个函数对应的具体的hook类型:
NF_IP_PRE_ROUTING
NF_IP_LOCAL_IN
NF_IP_FORWARD
NF_IP_LOCAL_OUT
NF_IP_POST_ROUTING
3、chain
相同类型的hook的所有操作以优先级升序排列所组成的链表。
4、match//匹配方式
1>标准匹配interface,ip address,protocol
2>由模块延伸出来的匹配
tcp协议高级匹配,udp协议高级匹配,MACaddress
ip报头中的TOS值匹配,匹配包中的数据内容。
5、table
netfilter中规则都存放在此结构中
1.3 主要源代码文件
Linux内核版本:2.4.x以上
Netfilter主文件:net/core/netfilter.c
Netfilter主头文件:include/linux/netfilter.h
IPv4相关:
c文件:net/ipv4/netfilter/*.c
头文件:include/linux/netfilter_ipv4.h
include/linux/netfilter_ipv4/*.h
IPv4协议栈主体的部分c文件,特别是与数据报传送过程有关的部分:
ip_input.c,ip_forward.c,ip_output.c,ip_fragment.c等
1.4 具体功能模块
- 数据报过滤模块
- 网络地址转换模块(NAT)
- 数据报修改模块(mangle)
- 连接跟踪模块(Conntrack)
- 其它高级功能模
FILTER,该模块的功能是过滤报文,不作任何修改,或者接受,或者拒绝。它在NF_IP_LOCAL_IN、NF_IP_FORWARD和NF_IP_LOCAL_OUT三处注册了钩子函数,也就是说,所有报文都将经过filter模块的处理。
NAT,网络地址转换(Network AddressTranslation),该模块以ConnectionTracking模块为基础,仅对每个连接的第一个报文进行匹配和处理,然后交由ConnectionTracking模块将处理结果应用到该连接之后的所有报文。nat在NF_IP_PRE_ROUTING、NF_IP_POST_ROUTING注册了钩子函数;如果需要,还可以在NF_IP_LOCAL_IN和NF_IP_LOCAL_OUT两处注册钩子,提供对本地报文(出/入)的地址转换。nat
MANGLE,属于可以进行报文内容修改的table,可供修改的报文内容包括MARK、TOS、TTL等,mangle表的操作函数嵌入在Netfilter的NF_IP_PRE_ROUTING,NF_IP_LOCAL_IN,NF_IP_FORWARD,NF_IP_LOCAL_OUT,NF_IP_POST_ROUTING五处。内核编程人员还可以通过注入模块,调用Netfilter的接口函数创建新的iptables。
ConnTrack,
2.1 Netfilter 介绍
Netfilter是Linux 2.4.x引入的一个子系统,它作为一个通用的、抽象的框架,提供一整套的hook函数的管理机制,使得诸如数据包过滤、网络地址转换(NAT)和基于协议类型的连接跟踪成为了可能。Netfilter主要通过表、链实现规则,可以这么说,Netfilter是表的容器,表是链的容器,链是规则的容器,最终形成对数据报处理规则的实现。Linux 2.6版内核的Netfilter目前支持IPv4、IPv6以及DECnet等协议栈,这里我们主要研究IPv4协议。Netfilter在内核中位置如下图所示:
这幅图很直观的反应了用户空间的iptables和内核空间的基于Netfilter的ip_tables模块之间的关系和其通讯方式,以及Netfilter在这其中所扮演的角色。
2.2 hook点
数据在协议栈里的发送过程中,从上至下依次是“加头”的过程,每到达一层数据就被会加上该层的头部;与此同时,接受数据方就是个“剥头”的过程,从网卡收上包来之后,在往协议栈的上层传递过程中依次剥去每层的头部,最终到达用户那儿的就是裸数据了。
对于收到的每个数据包,都从“A”点进来,经过路由判决,如果是发送给本机的就经过“B”点,然后往协议栈的上层继续传递;否则,如果该数据包的目的地是不本机,那么就经过“C”点,然后顺着“E”点将该包转发出去。
对于发送的每个数据包,首先也有一个路由判决,以确定该包是从哪个接口出去,然后经过“D”点,最后也是顺着“E”点将该包发送出去。
Netfilter在netfilter_ipv4.h中将这个五个点重新命了个名,这五个关键点我们就叫它们为hook点,如下图所示:
在每个hook点上,有很多已经按照优先级预先注册了的回调函数(称为“钩子函数”),形成了一条链。对于每个到来的数据包会依次被那些回调函数“操作”一番再视情况是将其放行,。但是无论如何,这些钩子函数最后必须向Netfilter报告一下该数据包的情况,每个钩子函数最后必须向Netfilter框架返回下列几个值其中之一:
#define NF_DROP 0 //丢弃数据包,不再传输
#define NF_ACCEPT 1 //允许数据包通行,交由下一个hook处理
#define NF_STOLEN 2 //数据包被送到上层协议处理,不在做netfilter
#define NF_QUEUE 3 //对该数据报进行排队(通常用于将数据报给用户空间的进程进行处理)
#define NF_REPEAT 4 //重复执行当前hook
#define NF_STOP 5 //允许数据包通行,但不再执行以后的hook
#define NF_MAX_VERDICT NF_STOP
这里面的NF_STOLEN不是很好理解,其实NF_DROP和NF_STOLEN的处理方式大致形同,都代表被阻止,不在被后面的流程发送。但NF_DROP会将skb使用的内存释放掉,以后都不能再处理skb了。而NF_STOLEN表示skb被hook函数处理,由hook函数决定skb是否发送,内存什么时候释放。一般是skb被修改时候使用这个返回值。此时skb的所有者变成了hook函数,原流程后面的函数将不再处理skb,netfilter决定“遗忘”skb,不再去管了。
另外,NF_REPEAT的使用要十分小心,因为同一个hook函数被反复执行,如果没有合适的结束机制,内核可能在这里死锁。
2.3 优先级
关于协议类型,hook点,hook函数,优先级,通过下面这个图给大家做个详细展示:
对于每种类型的协议,数据包都会依次按照hook点的方向进行传输,每个hook点上Netfilter又按照优先级挂了很多hook函数,这些hook函数就是用来处理数据包用的。
netfilter中不同的表在不同的hook点的优先级如下所示:
NF_IP_PRI_FIRST =INT_MIN, //优先级最高
简单说来,进入内核的数据包在经过不同的hook点时会遇到不同的处理机制,由Netfilter各个不同的表中规则对数据包进行处理。
2.4 全局变量的注册所谓的注册其本质上就是将数据存储到全局变量中,为后续的调用做好准备,Netfilter的注册机制可以分为表注册,target注册,match注册,hook操作注册。
2.4.1 表、target注册、match注册
表注册就是将定义好的表存放到一个全局变量xt的tables成员变量中,这个成员变量为一个xt_af的结构体,定义如下:
//这是一个存放所有规则信息及表信息的数据结构
struct xt_af {
};
static struct xt_af*xt;//一个全局变量,存储了所有内核和用户空间需要的规则信息。
与表注册类似,target注册和match注册也是通过向全局变量xt的成员变量写入信息来完成的。Netfilter通过这种注册的方式来实现对其扩展机制,用户可以根据自己的需求来实现match,target,甚至是自己实现一个表,然后注册到相应的全局变量,当数据包进入Netfilter后,hook操作会查找对应的表,以实现对数据包的匹配和处理。
对应注册接口(/linux/net/netfilter/x_table.c):
/* Registration hooks for targets. */
int xt_register_target(struct xt_target *target)
{
u_int8_t af = target->family;
int ret;
ret = mutex_lock_interruptible(&xt[af].mutex);
if (ret != 0)
return ret;
list_add(&target->list, &xt[af].target);
mutex_unlock(&xt[af].mutex);
return ret;
}
EXPORT_SYMBOL(xt_register_target);
<pre name="code" class="cpp" style="color: rgb(51, 51, 51); font-size: 14px; line-height: 21px; text-indent: 28px;">
<pre name="code" class="cpp" style="color: rgb(51, 51, 51); font-size: 14px; line-height: 21px; text-indent: 28px;">/* Registration hooks for match. */
int xt_register_match(struct xt_match *match){u_int8_t af = match->family;int ret;ret = mutex_lock_interruptible(&xt[af].mutex);if (ret != 0)return ret;list_add(&match->list, &xt[af].match);mutex_unlock(&xt[af].mutex);return ret;}EXPORT_SYMBOL(xt_register_match);
2.4.2 Hook注册
Hook操作的注册的地点与上面三个注册不同。先来看下什么是hook操作,Netfilter在不同的挂载点注册不同的操作函数,以达到为不同协议的不同挂载点的报文进行不同处理的目的,为实现这个目的Netfilter定义了一个叫做nf_hook_ops(include/linux/netfilter.h)的结构体,具体定义如下:
struct nf_hook_ops
{
};
其中hook成员变量的原型为:
typedef unsigned int nf_hookfn(unsigned int hooknum,
全局 nf_hooks和Hook注册接口(/linux/net/netfilter/core.c):
struct list_head nf_hooks[NPROTO][NF_MAX_HOOKS];//存放所有协议的hooks
nf_hooks定义了一个二维的结构体数组,用来存储不同协议栈钩子点的回调处理函数,其中,行数NPROTO为32,即目前内核所支持的最大协议簇;列数NF_MAX_HOOKS为挂载点的个数,目前在2.6内核中该值为8,按照priority值的大小从小到大排列。
nf_hooks数组的最终结构如下图所示。
在include/linux/socket.h中IP协议AF_INET(PF_INET)的序号为2,因此我们就可以得到IP协议钩子函数挂载点为:
PRE_ROUTING: nf_hooks[2][0]
LOCAL_IN: nf_hooks[2][1]
FORWARD: nf_hooks[2][2]
LOCAL_OUT: nf_hooks[2][3]
POST_ROUTING: nf_hooks[2][4]
int nf_register_hook(struct nf_hook_ops *reg)
{
struct nf_hook_ops *elem;
int err;
err = mutex_lock_interruptible(&nf_hook_mutex);
if (err < 0)
return err;
list_for_each_entry(elem, &nf_hooks[reg->pf][reg->hooknum], list) {
if (reg->priority < elem->priority)
break;
}
list_add_rcu(®->list, elem->list.prev);
mutex_unlock(&nf_hook_mutex);
#if defined(CONFIG_JUMP_LABEL)
static_key_slow_inc(&nf_hooks_needed[reg->pf][reg->hooknum]);
#endif
return 0;
}
EXPORT_SYMBOL(nf_register_hook);
举例:
static struct nf_hook_ops ipv4_defrag_ops[] = {
{
.hook = ipv4_conntrack_defrag,
.owner = THIS_MODULE,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_PRE_ROUTING,
.priority = NF_IP_PRI_CONNTRACK_DEFRAG,
},
{
.hook = ipv4_conntrack_defrag,
.owner = THIS_MODULE,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_LOCAL_OUT,
.priority = NF_IP_PRI_CONNTRACK_DEFRAG,
},
};
nf_register_hooks(ipv4_defrag_ops, ARRAY_SIZE(ipv4_defrag_ops));
2.4.3 netfilter 的内部的整个数据和扩展流程:
Netfilter将不同协议的不同挂载点的操作函数都存放在一个全局变量nf_hooks中,数据包进入Netfilter后,会查找相应的hook操作。
对于所有从本机发出去的报文都会首先去 Netfilter 的 nf_hooks[2][3] 过滤点去过滤。一般情况下来来说,不管是路由器还是 PC 终端,很少有人限制自己机器发出去的报文。因为这样做的潜在风险也是显而易见的,往往会因为一些不恰当的设置导致某些服务失效,所以在这个过滤点上拦截数据包的情况非常少。当然也不排除真的有特殊需求的情况。
整个Linux内核中Netfilter框架的HOOK机制可以概括如下:
在数据包流经内核协议栈的整个过程中,在一些已预定义的关键点上PRE_ROUTING、LOCAL_IN、FORWARD、LOCAL_OUT和POST_ROUTING会根据数据包的协议簇PF_INET到这些关键点去查找是否注册有钩子函数。如果没有,则直接返回okfn函数指针所指向的函数继续走协议栈;如果有,则调用nf_hook_slow函数,从而进入到Netfilter框架中去进一步调用已注册在该过滤点下的钩子函数,再根据其返回值来确定是否继续执行由函数指针okfn所指向的函数。
当内核编译时CONFIG_NETFILTER宏定义时,即启用netfilter时,代码如下:
#define NF_HOOK(pf, hook, skb, indev, outdev, okfn)\
NF_HOOK_THRESH(pf,hook, skb, indev, outdev, okfn, INT_MIN)
NF_HOOK实际上调用了NF_HOOK_THRESH。NF_HOOK_THRESH也是一个宏定义,我们发现NF_HOOK_THRESH宏只增加了一个thresh参数,这个参数就是用来指定通过该宏去遍历钩子函数时的优先级,同时,该宏内部又调用了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函数的参数:
pf | 网络协议编号 |
hook | 所属hook点,HOOK点的名字,对于IP层,就是取上面的五个值; |
pskb | 要处理的数据包,网络设备数据缓存区 |
indev | 数据包进入的接口 |
outdev | 数据包发出的接口 |
okfn | Netfilter处理完后要运行的函数 |
thresh | 优先级过滤,只有thresh小于优先级的hook函数才能调用 |
cond | 条件判断,当cond为0时直接当NF_ACCEPT处理 |
还有一个宏定义NF_HOOK_COND。
#define NF_HOOK_COND(pf, hook, skb, indev, outdev,okfn, cond) \
({int __ret; \
if ((__ret=nf_hook_thresh(pf, hook, &(skb),indev, outdev, okfn, INT_MIN, cond)) == 1)\
__ret= (okfn)(skb); \
__ret;})
由上面的带码,我们可以看到。实际上所有的钩子函数都调用了nf_hook_thresh。当nf_hook_thresh返回值是1是,调用okfn处理skb。
还有一个函数也调用了nf_hook_thresh,那就是nf_hook。
static inline int nf_hook(int pf, unsigned inthook, struct sk_buff **pskb,
struct net_device *indev, struct net_device*outdev,
int (*okfn)(struct sk_buff *))
{
returnnf_hook_thresh(pf, hook, pskb, indev, outdev, okfn, INT_MIN, 1);
}
当没有使用netfilter时,这几个钩子就比较简单了。
#define NF_HOOK(pf, hook, skb, indev, outdev, okfn)(okfn)(skb)
#define NF_HOOK_COND(pf, hook, skb, indev, outdev,okfn, cond) (okfn)(skb)
static inline int nf_hook_thresh(int pf, unsignedint hook,
struct sk_buff **pskb,
struct net_device *indev,
struct net_device *outdev,
int (*okfn)(struct sk_buff *), int thresh,
int cond)
{
returnokfn(*pskb);
}
static inline int nf_hook(int pf, unsigned inthook, struct sk_buff **pskb,
struct net_device *indev, struct net_device*outdev,
int (*okfn)(struct sk_buff *))
{
return1;
}
当不使用netfilter,nf_hook简单的返回1。而其他钩子函数调用okfn(*pskb)。Okfn就是前面提到的B函数,pskb是要处理的数据包。由此可见,此时的netfilter退化为简单调用okfn。比如在ip_input.c文件中:
return NF_HOOK(PF_INET, NF_IP_LOCAL_IN, skb,skb->dev, NULL,
ip_local_deliver_finish);
实际的效果就是ip_local_deliver_finish(skb)。
这里我们详细分析一下nf_hook_thresh和相关函数。
static inline int nf_hook_thresh(int pf, unsignedint hook,
struct sk_buff **pskb,
struct net_device *indev,
struct net_device *outdev,
int (*okfn)(struct sk_buff *), int thresh,
int cond)
{
if(!cond)
return1;
#ifndef CONFIG_NETFILTER_DEBUG
if(list_empty(&nf_hooks[pf][hook]))
return1;
#endif
returnnf_hook_slow(pf, hook, pskb, indev, outdev, okfn, thresh);
}
在这个函数里我们可以看到cond参数的用途,当cond值为0时,不做任何netfilter处理直接返回1.
nf_hook_thresh最后调用的是nf_hook_slow函数。
int nf_hook_slow(int pf, unsigned int hook, structsk_buff **pskb,
struct net_device *indev,
struct net_device *outdev,
int (*okfn)(struct sk_buff *),
int hook_thresh)
{
structlist_head *elem;
unsignedint verdict;
intret = 0;
/* Wemay already have this, but read-locks nest anyway */
rcu_read_lock();
elem= &nf_hooks[pf][hook];
next_hook:
verdict= nf_iterate(&nf_hooks[pf][hook], pskb, hook, indev,
outdev, &elem, okfn, hook_thresh); //执行netfilter的处理函数
if(verdict == NF_ACCEPT || verdict == NF_STOP) {
ret= 1; //当netfilter的返回值为NF_ACCEPT和NF_STOP直接通过
gotounlock;
}else if (verdict == NF_DROP) {
kfree_skb(*pskb);//当netfilter的返回值为NF_DROP,将数据包丢弃
ret= -EPERM;
}else if ((verdict & NF_VERDICT_MASK) == NF_QUEUE) {
//当返回值是NF_QUEUE,将数据包插入ip_queue队列,插入的队列的数据包
可以用netlink接口从用户态读出。可以实现用户态防火墙
NFDEBUG("nf_hook:Verdict = QUEUE.\n");
if(!nf_queue(pskb, elem, pf, hook, indev, outdev, okfn,
verdict >> NF_VERDICT_BITS))
gotonext_hook;
}
unlock:
rcu_read_unlock();
returnret;
}
由代码我们可以看到,nf_hook_slow调用nf_iterate来对每个协议的每个hook点的函数处理。
unsigned int nf_iterate(struct list_head *head,
structsk_buff **skb,
inthook,
conststruct net_device *indev,
conststruct net_device *outdev,
structlist_head **i,
int(*okfn)(struct sk_buff *),
inthook_thresh)
{
unsignedint verdict;
/*
* The caller must not block between calls tothis
* function because of risk of continuing fromdeleted element.
*/
list_for_each_continue_rcu(*i,head) {
structnf_hook_ops *elem = (struct nf_hook_ops *)*i;
if(hook_thresh > elem->priority)
continue;
/*Optimization: we don't need to hold module
reference here, since function can't sleep. --RR */
verdict= elem->hook(hook, skb, indev, outdev, okfn); //调用了注册的hook函数
if(verdict != NF_ACCEPT) {
#ifdef CONFIG_NETFILTER_DEBUG
if(unlikely((verdict & NF_VERDICT_MASK)
>NF_MAX_VERDICT)) {
NFDEBUG("Evilreturn from %p(%u).\n",
elem->hook, hook);
continue;
}
#endif
if(verdict != NF_REPEAT){
IPV4_DEBUG_OUT(IPV4_DEBUG_NFCORE,"NF-CORE:hook= %d, elem->pri = %d verdict = %d\n",
hook,elem->priority, verdict);
IPV4_DEBUG_OUT(IPV4_DEBUG_NFCORE," indev = %s, outdev = %s\n",
indev?indev->name:"NULL", outdev?outdev->name:"NULL");
returnverdict;
}
*i= (*i)->prev; //当返回值是NF_REPEAT,重新执行hook
}
}
returnNF_ACCEPT;
}
由代码可以看出,hook_thresh的作用是一个优先级过滤,只有优先级小于elem->priority的hook才能执行。
2.7 netfilter的装载和卸载
自定义hook的装载机卸载,init_module()和cleanup_module,编译出.o(test)文件,使用insmod test ,insmod会自动查找.o文件中定义的init_module,进行自定义模块的加载,执行rmmod test,rmmod命令会自动执行模块中的cleanup_module.
三、数据传输流程
3.1 内核中注册的hook回调函数
数据包在协议栈中传递时会经过不同的HOOK点,而每个HOOK点上又被Netfilter预先注册了一系列hook回调函数,当每个数据包到达这些点后会被这些hook函数轮番处理一番。目前系统中已经注册了的hook函数可分为以下几类:
它们在协议栈中位置如下:
上图详细给出了ip_tables内核模块中那些hook函数在各个hook点分布情况。所有由网卡收上来的数据包率先被ip_conntrack_defrag处理;链接跟踪系统的入口函数以-200的优先级被注册到了PRE_ROUTING和LOCAL_OUT两个hook点上,且其优先级高于mangle操作,NAT和包过滤等其他模块;DNAT可以在PRE_ROUTING和LOCAL_OUT两个hook点来做,SNAT可以在LOCAL_IN和POST_ROUTING两个hook点上。3.2 Hook函数调用流程
-
NF_IP_PRE_ROUTING (0)
数据报在进入路由代码被处理之前,数据报在IP数据报接收函数ip_rcv()(位于net/ipv4/ip_input.c,Line379)的最后,也就是在传入的数据报被处理之前经过这个HOOK。在ip_rcv()中挂接这个HOOK之前,进行的是一些与类型、长度、版本有关的检查。
经过这个HOOK处理之后,数据报进入ip_rcv_finish()(位于net/ipv4/ip_input.c,Line306),进行查路由表的工作,并判断该数据报是发给本地机器还是进行转发。
在这个HOOK上主要是对数据报作报头检测处理,以捕获异常情况。
涉及功能(优先级顺序):Conntrack(-200)、mangle(-150)、DNAT(-100)
-
NF_IP_LOCAL_IN (1)
目的地为本地主机的数据报在IP数据报本地投递函数ip_local_deliver()(位于net/ipv4/ip_input.c,Line290)的最后经过这个HOOK。
经过这个HOOK处理之后,数据报进入ip_local_deliver_finish()(位于net/ipv4/ip_input.c,Line219)
这样,IPTables模块就可以利用这个HOOK对应的INPUT规则链表来对数据报进行规则匹配的筛选了。防火墙一般建立在这个HOOK上。
涉及功能:mangle(-150)、filter(0)、SNAT(100)、Conntrack(INT_MAX-1)
-
NF_IP_FORWARD (2)
目的地非本地主机的数据报,包括被NAT修改过地址的数据报,都要在IP数据报转发函数ip_forward()(位于net/ipv4/ip_forward.c,Line73)的最后经过这个HOOK。
经过这个HOOK处理之后,数据报进入ip_forward_finish()(位于net/ipv4/ip_forward.c,Line44)
另外,在net/ipv4/ipmr.c中的ipmr_queue_xmit()函数(Line1119)最后也会经过这个HOOK。(ipmr为多播相关,估计是在需要通过路由转发多播数据时的处理)
这样,IPTables模块就可以利用这个HOOK对应的FORWARD规则链表来对数据报进行规则匹配的筛选了。
涉及功能:mangle(-150)、filter(0)
-
NF_IP_LOCAL_OUT (3)
本地主机发出的数据报在IP数据报构建/发送函数ip_queue_xmit()(位于net/ipv4/ip_output.c,Line339)、以及ip_build_and_send_pkt()(位于net/ipv4/ip_output.c,Line122)的最后经过这个HOOK。(在数据报处理中,前者最为常用,后者用于那些不传输有效数据的SYN/ACK包)
经过这个HOOK处理后,数据报进入ip_queue_xmit2()(位于net/ipv4/ip_output.c,Line281)
另外,在ip_build_xmit_slow()(位于net/ipv4/ip_output.c,Line429)和ip_build_xmit()(位于net/ipv4/ip_output.c,Line638)中用于进行错误检测;在igmp_send_report()(位于net/ipv4/igmp.c,Line195)的最后也经过了这个HOOK,进行多播时相关的处理。
这样,IPTables模块就可以利用这个HOOK对应的OUTPUT规则链表来对数据报进行规则匹配的筛选了。
涉及功能:Conntrack(-200)、mangle(-150)、DNAT(-100)、filter(0)
-
NF_IP_POST_ROUTING (4)
所有数据报,包括源地址为本地主机和非本地主机的,在通过网络设备离开本地主机之前,在IP数据报发送函数ip_finish_output()(位于net/ipv4/ip_output.c,Line184)的最后经过这个HOOK。
经过这个HOOK处理后,数据报进入ip_finish_output2()(位于net/ipv4/ip_output.c,Line160)另外,在函数ip_mc_output()(位于net/ipv4/ip_output.c,Line195)中在克隆新的网络缓存skb时,也经过了这个HOOK进行处理。
涉及功能:mangle(-150)、SNAT(100)、Conntrack(INT_MAX)
四、Netfilter内部处理流程1 )、 net/ipv4/ip_input.c 里的 ip_rcv 函数。该函数主要用来处理网络层的 IP 报文的入口函数,它到 Netfilter 框架的切入点为:
NF_HOOK(PF_INET, NF_IP_PRE_ROUTING, skb, dev, NULL,ip_rcv_finish)
/*
* Main IP Receive routine.
*/
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)
{
.....
<span style="white-space:pre"> </span>return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,
<span style="white-space:pre"> </span> ip_rcv_finish);
csum_error:
<span style="white-space:pre"> </span>IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_CSUMERRORS);
inhdr_error:
<span style="white-space:pre"> </span>IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INHDRERRORS);
drop:
<span style="white-space:pre"> </span>kfree_skb(skb);
out:
<span style="white-space:pre"> </span>return NET_RX_DROP;
}
根据前面的理解,这句代码意义已经很直观明确了。那就是:如果协议栈当前收到了一个 IP 报文 (PF_INET) ,那么就把这个报文传到 Netfilter 的 NF_IP_PRE_ROUTING 过滤点,去检查在那个过滤点 (nf_hooks[2][0]) 是否已经有人注册了相关的用于处理数据包的钩子函数。如果有,则挨个遍历链表 nf_hooks[2][0] 去寻找匹配的 match 和相应的 target ,根据返回到 Netfilter 框架中的值来进一步决定该如何处理该数据包 ( 由钩子模块处理还是交由 ip_rcv_finish 函数继续处理 ) 。刚才说到所谓的“检查”。其核心就是 nf_hook_slow() 函数。该函数本质上做的事情很简单,根据优先级查找双向链表 nf_hooks[][] ,找到对应的回调函数来处理数据包:
struct list_head **i;
list_for_each_continue_rcu(*i, head) {
struct nf_hook_ops *elem = (struct nf_hook_ops*)*i;
if (hook_thresh > elem->priority)
continue;
verdict = elem->hook(hook, skb, indev, outdev, okfn);
if (verdict != NF_ACCEPT) { … … }
return NF_ACCEPT;
}
上面的代码是 net/netfilter/core.c 中的 nf_iterate() 函数的部分核心代码,该函数被 nf_hook_slow 函数所调用,然后根据其返回值做进一步处理。
2 )、 net/ipv4/ip_forward.c 中的 ip_forward 函数,它的切入点为:
NF_HOOK(PF_INET, NF_IP_FORWARD, skb, skb->dev,rt->u.dst.dev,ip_forward_finish);
<pre name="code" class="cpp">int ip_forward(struct sk_buff *skb)
{
struct iphdr *iph; /* Our header */
struct rtable *rt; /* Route we use */
struct ip_options *opt = &(IPCB(skb)->opt);
...........................
<span style="white-space:pre"> </span>return NF_HOOK(NFPROTO_IPV4, NF_INET_FORWARD, skb, skb->dev,
<span style="white-space:pre"> </span> rt->dst.dev, ip_forward_finish);
sr_failed:
<span style="white-space:pre"> </span> icmp_send(skb, ICMP_DEST_UNREACH, ICMP_SR_FAILED, 0);
<span style="white-space:pre"> </span> goto drop;
too_many_hops:
<span style="white-space:pre"> </span>/* Tell the sender its packet died... */
<span style="white-space:pre"> </span>IP_INC_STATS_BH(dev_net(skb_dst(skb)->dev), IPSTATS_MIB_INHDRERRORS);
<span style="white-space:pre"> </span>icmp_send(skb, ICMP_TIME_EXCEEDED, ICMP_EXC_TTL, 0);
drop:
<span style="white-space:pre"> </span>kfree_skb(skb);
<span style="white-space:pre"> </span>return NET_RX_DROP;
}
在经过路由抉择后,所有需要本机转发的报文都会交由
ip_forward
函数进行处理。这里,该函数由
NF_IP_FOWARD
过滤点切入到
Netfilter
框架,在
nf_hooks[2][2]
过滤点执行匹配查找。最后根据返回值来确定
ip_forward_finish
函数的执行情况。
3 )、 net/ipv4/ip_output.c 中的 ip_output 函数,它切入 Netfilter 框架的形式为:
NF_HOOK_COND(PF_INET, NF_IP_POST_ROUTING, skb,NULL, dev,ip_finish_output,
!(IPCB(skb)->flags & IPSKB_REROUTED));
int ip_output(struct sk_buff *skb)
{
struct net_device *dev = skb_dst(skb)->dev;
IP_UPD_PO_STATS(dev_net(dev), IPSTATS_MIB_OUT, skb->len);
skb->dev = dev;
skb->protocol = htons(ETH_P_IP);
return NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING, skb, NULL, dev,
ip_finish_output,
!(IPCB(skb)->flags & IPSKB_REROUTED));
}
EXPORT_SYMBOL(ip_output); //
这里我们看到切入点从无条件宏 NF_HOOK 改成了有条件宏 NF_HOOK_COND ,调用该宏的条件是:如果协议栈当前所处理的数据包 skb 中没有重新路由的标记,数据包才会进入 Netfilter 框架。否则直接调用 ip_finish_output 函数走协议栈去处理。除此之外,有条件宏和无条件宏再无其他任何差异。
如果需要陷入 Netfilter 框架则数据包会在 nf_hooks[2][4] 过滤点去进行匹配查找。
4 )、还是在 net/ipv4/ip_input.c 中的 ip_local_deliver 函数。该函数处理所有目的地址是本机的数据包,其切入函数为:
NF_HOOK(PF_INET, NF_IP_LOCAL_IN, skb, skb->dev,NULL,ip_local_deliver_finish);
/*
* Deliver IP Packets to the higher protocol layers.
*/
int ip_local_deliver(struct sk_buff *skb)
{
/*
* Reassemble IP fragments.
*/
if (ip_is_fragment(ip_hdr(skb))) {
if (ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER))
return 0;
}
return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN, skb, skb->dev, NULL,
ip_local_deliver_finish);
}
发给本机的数据包,首先全部会去
nf_hooks[2][1]
过滤点上检测是否有相关数据包的回调处理函数,如果有则执行匹配和动作,最后根据返回值执行
ip_local_deliver_finish
函数。
5 )、 net/ipv4/ip_output.c 中的 ip_push_pending_frames 函数。该函数是将 IP 分片重组成完整的 IP 报文,然后发送出去。
4.2 IP数据包处理流程
要想理解Netfilter的工作原理,必须从对Linux IP报文处理流程的分析开始,Netfilter正是将自己紧密地构建在这一流程之中的。IP
报文接收从网卡驱动程序开始,当网卡收到一个报文时,会产生一个中断,其驱动程序中的中断服务程序将调用确定的接收函数来处理。流程分成两个阶段:驱动程序中断服务程序阶段和IP协议栈处理阶段,驱动程序的处理流程与本章的联系不是十分紧密,故不做详细的介绍,我们以下面的流程图简要介绍以下数据包的接收过程。
驱动程序处理报文时,会生成一个skb_buff,同时将其放入一个全局的存储结构当中,同时设置软中断NET_RX_SOFTIRQ等待内核处理,内核收到软中断后,报文便开始了协议栈之旅。我们用以下的流程图来表示接收报文时整个处理的流程:
从下图的流程可以看出,Netfilter以NF_HOOK形式挂载到ip协议栈对报文的处理过程中,然后将相应的数据包转入到Netfilter中来处理,我们可以将IP协议栈中调用NF_HOOK的地方称之为挂载点。除了流程图中指出的几处挂载点,Netfitler还在ip协议栈的多处进行了挂载,可以具体参考内核网络部分的源码。