Netfilter 框架
目录
1 网络通信
1.1 网络通信的基本模型
1.2 协议栈底层机制
2 Netfilter
2.1Netfilter 介绍
2.2 钩子函数返回值
2.3 hook 点
2.4 协议栈切入 Netfilter 框架
3 Netfilter 的实现方式
3.1 nf_hooks[][]结构
3.2 nf_hook_ops
3.3 增加新的钩子函数
3.4 基于源接口的数据包过滤钩子函数
4 iptables 防火墙内核模块
4.1 iptables
4.2 Netfilter 框架防火墙 iptables hook 函数分类
4.3 iptables 基础
4.4 iptables 命令格式
5 连接跟踪机制
5.1.重要数据结构
5.2 重要函数
5.3 链接跟踪建立的三条路径
5.4 IP 层接收和发送数据包进入连接跟踪钩子函数的入口
5.5 连接跟踪的流程分析
1 网络通信
1.1 网络通信的基本模型
在数据的发送过程中,从上至下依次是“加头”的过程,每到达一层数据就被会加上该
层的头部;与此同时,接受数据方就是个“剥头”的过程,从网卡收上包来之后,在往协议
栈的上层传递过程中依次剥去每层的头部,最终到达用户那儿的就是裸数据了。
1.2 协议栈底层机制
“栈”模式底层机制基本就是像下面这个样子:
对于收到的每个数据包,都从“A”点进来,经过路由判决,如果是发送给本机的就经
过“B”点,然后往协议栈的上层继续传递;否则,如果该数据包的目的地是不本机,那么
就经过“C”点,然后顺着“E”点将该包转发出去。
对于发送的每个数据包,首先也有一个路由判决,以确定该包是从哪个接口出去,然后
经过“D”点,最后也是顺着“E”点将该包发送出去。
协议栈那五个关键点 A,B,C,D 和 E 就是我们 Netfilter 大展拳脚的地方了。
2 Netfilter
2.1Netfilter 介绍
Netfilter 是 Linux 2.4.x 引入的一个子系统,它作为一个通用的、抽象的框架,提供一整
套的 hook 函数的管理机制,使得诸如数据包过滤、网络地址转换(NAT)和基于协议类型的
连接跟踪成为了可能。Netfilter 在内核中位置如下图所示:
这幅图,很直观的反应了用户空间的 iptables 和内核空间的基于 Netfilter 的 ip_tables 模
块之间的关系和其通讯方式,以及 Netfilter 在这其中所扮演的角色。
Netfilter 在 netfilter_ipv4.h 中将那五个关键点“ABCDE”上来。重新命名,如下图所示。
2.2 钩子函数返回值
在每个关键点上,有很多已经按照优先级预先注册了的回调函数(这些函数称为“钩子
函数”)埋伏在这些关键点,形成了一条链。对于每个到来的数据包会依次被那些回调函数
“调戏”一番再视情况是将其放行,丢弃还是怎么滴。但是无论如何,这些回调函数最后必
须向 Netfilter 报告一下该数据包的死活情况,因为毕竟每个数据包都是 Netfilter 从人家协议
栈那儿借调过来给兄弟们 Happy 的,别个再怎么滴也总得“活要见人,死要见尸”吧。每
个钩子函数最后必须向 Netfilter 框架返回下列几个值其中之一:
n NF_ACCEPT 继续正常传输数据报。这个返回值告诉 Netfilter:到目前为止,该数
据包还是被接受的并且该数据包应当被递交到网络协议栈的下一个阶段。
n NF_DROP 丢弃该数据报,不再传输。
n NF_STOLEN 模块接管该数据报,告诉 Netfilter“忘掉”该数据报。该回调函数将
从此开始对数据包的处理,并且 Netfilter 应当放弃对该数据包做任何的处理。但是,这并不
意味着该数据包的资源已经被释放。这个数据包以及它独自的 sk_buff 数据结构仍然有效,
只是回调函数从 Netfilter 获取了该数据包的所有权。
n NF_QUEUE 对该数据报进行排队(通常用于将数据报给用户空间的进程进行处理)
n NF_REPEAT 再次调用该回调函数,应当谨慎使用这个值,以免造成死循环。
上面提到的五个关键点后面我们就叫它们为 hook 点,每个 hook 点所注册的那些回调函数都
将其称为 hook 函数。
2.3 hook 点
Linux 2.6 版内核的 Netfilter 目前支持 IPv4、IPv6 以及 DECnet 等协议栈,这里我们主要
研究 IPv4 协议。关于协议类型,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 各个参数的解释说明:
1) pf:协议族名,Netfilter 架构同样可以用于 IP 层之外,因此这个变量还可
以有诸如 PF_INET6,PF_DECnet 等名字。
2) hook:HOOK 点的名字,对于 IP 层,就是取上面的五个值;
3) skb:不解释;
4) indev:数据包进来的设备,以 struct net_device 结构表示;
5) outdev:数据包出去的设备,以 struct net_device 结构表示;
(后面可以看到,以上五个参数将传递给 nf_register_hook 中注册的处理函数。)
6) okfn:是个函数指针,当所有的该 HOOK 点的所有登记函数调用完后,转而
走此流程。
NF_HOOK_THRESH 又是一个宏:#define NF_HOOK_THRESH(pf, hook, skb, indev, outdev,
okfn, thresh)
我们发现 NF_HOOK_THRESH 宏只增加了一个 thresh 参数,这个参数就是用来指定通
过该宏去遍历钩子函数时的优先级,同时,该宏内部又调用了 nf_hook_thresh 函数:
这个函数又只增加了一个参数 cond,该参数为 0 则放弃遍历,并且也不执行 okfn 函数;
为 1 则执行 nf_hook_slow 去完成钩子函数 okfn 的顺序遍历(优先级从小到大依次执行)。
在 net/netfilter/core.h 文件中定义了一个二维的结构体数组,用来存储不同协议栈钩子点的回
调处理函数。
struct list_head nf_hooks[NPROTO][NF_MAX_HOOKS];
其中,行数 NPROTO 为 32,即目前内核所支持的最大协议簇;列数 NF_MAX_HOOKS 为
挂载点的个数,目前在 2.6 内核中该值为 8。nf_hooks 数组的最终结构如下图所示。
在 include/linux/socket.h 中 IP 协议 AF_INET(PF_INET)的序号为 2,因此我们就可以得
到 TCP/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]
2.4 协议栈切入 Netfilter 框架
在 2.6 内核的 IP 协议栈里,从协议栈正常的流程切入到 Netfilter 框架中,然后顺序、依
次去调用每个 HOOK 点所有的钩子函数的相关操作有如下几处:
1)、net/ipv4/ip_input.c 里的 ip_rcv 函数。该函数主要用来处理网络层的 IP 报文的入口
函数,它到 Netfilter 框架的切入点为:
NF_HOOK(PF_INET, NF_IP_PRE_ROUTING, skb, dev, NULL,ip_rcv_finish)
根据前面的理解,这句代码意义已经很直观明确了。那就是:如果协议栈当前收到了一个
IP 报文(PF_INET),那么就把这个报文传到 Netfilter 的 NF_IP_PRE_ROUTING 过滤点,去
检查[R]在那个过滤点(nf_hooks[2][0])是否已经有人注册了相关的用于处理数据包的钩子函
数。如果有,则挨个去遍历链表 nf_hooks[2][0]去寻找匹配的 match 和相应的 target,根据返
回到 Netfilter 框架中的值来进一步决定该如何处理该数据包(由钩子模块处理还是交由
ip_rcv_finish 函数继续处理)。
[R]:刚才说到所谓的“检查”。其核心就是 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);
在经过路由抉择后,所有需要本机转发的报文都会交由 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));
这里我们看到切入点从无条件宏 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);
发给本机的数据包,首先全部会去 nf_hooks[2][1]过滤点上检测是否有相关数据包的回调处
理函数,如果有则执行匹配和动作,最后根据返回值执行 ip_local_deliver_finish 函数。
5)、net/ipv4/ip_output.c 中的 ip_push_pending_frames 函数。该函数是将 IP 分片重组成
完整的 IP 报文,然后发送出去。进入 Netfilter 框架的切入点为:
NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, skb->dst->dev, dst_output);
对于所有从本机发出去的报文都会首先去 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 所指向的函数。
3 Netfilter 的实现方式
3.1 nf_hooks[][]结构
用于存储不同协议簇在每个 hook 点上所注册的 hook 函数链的二维数组 nf_hooks[][],
其类型为 list_head:
struct list_head nf_hooks[NPROTO][NF_MAX_HOOKS];
list_head{}结构体定义在 include/linux/list.h 头文件中
struct list_head {
struct list_head *next, *prev;
};
这是 Linux 内核中处理双向链表的标准方式。当某种类型的数据结构需要被组织成双向链表
时,会在该数据结构的第一个字段放置一个 list_head{}类型的成员。在后面的使用过程中可
以通过强制类型转换来实现双向链表的遍历操作。
3.2 nf_hook_ops
在 Netfilter 中一个非常重要的数据结构是 nf_hook_ops{} :
struct nf_hook_ops
{
struct list_head list;
/* User fills in from here down. */
nf_hookfn *hook;
struct module *owner;
int pf;
int hooknum;
/* Hooks are ordered in ascending priority. */
int priority;
};
对该结构体中的成员参数做一下解释:
n list:因为在一个 HOOK 点有可能注册多个钩子函数,因此这个变量用来将某个 HOOK
点所注册的所有钩子函数组织成一个双向链表;
n hook:该参数是一个指向 nf_hookfn 类型的函数的指针,由该函数指针所指向的回调函
数在该 hook 被激活时调用【nf_hookfn 在后面做解释】;
n owner:表示这个 hook 是属于哪个模块的
n pf:该 hook 函数所处理的协议。目前我们主要处理 IPv4,所以该参数总是 PF_INET;
n hooknum:钩子函数的挂载点,即 HOOK 点;
n priority:优先级。前面也说过,一个 HOOK 点可能挂载了多个钩子函数,当 Netfilter 在
这些 HOOK 点上遍历查找所注册的钩子函数时,这些钩子函数的先后执行顺序便由该参数
来制定。
nf_hookfn 所定义的回调函数的原型在 include/linux/netfilter.h 文件中:
typedef unsigned int nf_hookfn(unsigned int hooknum, //HOOK 点
struct sk_buff **skb, //不解释
const struct net_device *in, //数据包的网络如接口
const struct net_device *out, //数据包的网络出接口
int (*okfn)(struct sk_buff *)); //后续的处理函数
我们可以到,上面这五个参数最后将由 NF_HOOK 或 NF_HOOK_COND 宏传递到 Netfilter
框架中去。
3.3 增加新的钩子函数
如果要增加新的钩子函数到 Netfilter 中相应的过滤点,我们要做的工作其实很简单:
1)、编写自己的钩子函数;
2)、实例化一个 struct nf_hook_ops{}结构,并对其进行适当的填充,第一个参数 list
并不是用户所关心的,初始化时必须设置成{NULL,NULL};
3)、用 nf_register_hook()函数将我们刚刚填充的 nf_hook_ops 结构
体注册到相应的 HOOK 点上,即 nf_hooks[prot][hooknum]。
内核在网络协议栈的关键点引入 NF_HOOK 宏,从而搭建起了整个 Netfilter 框架。但
是 NF_HOOK 宏仅仅只是一个跳转而已,更重要的内容是“内核是如何注册钩子函数的呢?
这些钩子函数又是如何被调用的呢?谁来维护和管理这些钩子函数?”
3.4 基于源接口的数据包过滤钩子函数
/*
* 安装一个丢弃所有进入我们指定接口的数据包的 Netfilter hook 函数的示例代码
/* 用于注册我们的函数的数据结构*/
static struct nf_hook_ops nfho;
/*丢弃的数据包来自的接口的名字*/
static char *drop_if = "lo";
/* 注册的 hook 函数的实现*/
unsigned int hook_func(unsigned int hooknum,
struct sk_buff **skb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
if (strcmp(in->name, drop_if) == 0) {
printk("Dropped packet on %s...\n", drop_if);
return NF_DROP;
} else {
return NF_ACCEPT;
}
}
/* 初始化程序*/
int init_module()
{
/* 填充我们的 hook 数据结构*/
nfho.hook = hook_func; /* 处理函数*/
nfho.hooknum = NF_IP_PRE_ROUTING; /* 使用 IPv4 的第一个 hook */
nfho.pf = PF_INET;
nfho.priority = NF_IP_PRI_FIRST; /* 让我们的函数首先执行*/
nf_register_hook(&nfho);
return 0;