1.Netfilter简介
Netfilter是Linux2.4之后加入到内核的防火墙框架,该框架简洁灵活,实现了安全策略中的许多动能:Netfilter子系统提供了有状态的或无状态的分组过滤, 网络地址转换(NAT),IP地址伪装, 还具备为高级选路、连接状态管理而变形(mangle)IP 头信息的能力和基于数据类型的连接追踪等。
如果把netfilter比作一个加工厂,工厂有很多工站和一整套的加工流程,每个工站都有一套处理的准则,原料到达某个工站之后就可能会被丢掉,修改或者会被送到其他工站或下一工站,知道最后产品的出厂。那么工厂的原料就是网络接口接收到数据(skb),各工站就相当于要实现对数据处理的函数,而处理的动作就包含了丢弃、修改、转发等。
Netfilter主要采用连接跟踪(Connection Tracking)、包过滤(Packet Filtering)、地址转换(NAT)、包处理(Packet Mangling)4种关键技术。
连接跟踪:连接跟踪被用来记录和跟踪连接的状态, 它一个tuple(源地址/端口+目的地址/端口)四元组来标识一个连接。并将连接的信息记录到连接跟踪表中(连接跟踪表是一个记录所有数据包连接信息的Hash散列表),连接跟中表本身仅仅是记录各连接的状态,本身不具备过滤功能,但是可以为过滤提供匹配规则。
包过滤:检查通过Netfilter的每个数据包的头部,然后决定如何处置它们,可以选择丢弃,让包通过,或者更复杂的操作。
地址转换:是将IP 数据包头中的IP 地址转换为另一个IP 地址的过程。网络地址转换又分为SNAT(源地址转换)和DNAT(目的地址转换)。SNAT是指修改数据包的源地址(改变连接的源IP)。SNAT会在数据包送出之前的最后一刻做好转换工作。DNAT 是指修改数据包的目标地址(改变连接的目的IP)。DNAT 总是在数据包进入以后立即完成转换
包处理:利用包处理可以设置或改变数据包的服务类型(Type of Service,TOS)字段;改变包的生存期(Time to Live,TTL)字段;在包中设置标志值,利用该标志值可以进行带宽限制和分类查询。
3.Netfilter的框架
Netfilter的架构就是在整个网络流程的若干位置放置了一些检测点(HOOK ),而在每个检测点上可以登记了一些回调处理函数来实现一些数据处理的功能(如包过滤,NAT等)。Netfilter通用框架不依赖于具体的协议,而是为每种网络协议定义一套HOOK函数。针对Ipv4定义了5个钩子函数在数据报流过协议栈的5个关键点被调用,下面有关Netfilter的说明都是基于IPv4协议的。
图1. Netfilter 架构图
如图1所示:粉色方框即为数据流程中的HOOK点,数据按照箭头方向进行处理。
4. HOOK点回调函数的调用与注册
- 调用
在分析数据流的处理过程中,会经遇到NF_HOOK宏,这个宏就是将数据流导向到Netfilter来进行处理的一个入口,在这个宏中将会实现对指定HOOK点所挂载回调函数的调用,下面我们分析一下这个宏如何调用回调函数的。
以net/ipv4/ip_input.c文件,函数ip_rcv中,所调用的下面的代码为例进行说明:
return NF_HOOK(PF_INET, NF_INET_PRE_ROUTING, skb, dev, NULL,
ip_rcv_finish);
我们可以在include/linux/netfilter.h中找到该宏的定义:
#ifdef CONFIG_NETFILTER
……
#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;})
#define NF_HOOK(pf, hook, skb, indev, outdev, okfn) \
NF_HOOK_THRESH(pf, hook, skb, indev, outdev, okfn, INT_MIN)
……
#else /* !CONFIG_NETFILTER */
#define NF_HOOK(pf, hook, skb, indev, outdev, okfn) (okfn)(skb)
……
#endif
参数说明:
1)pf:协议族名,Netfilter架构同样可以用于IP层之外,因此这个变量还可以有诸如PF_INET6,PF_DECnet等名字。
2)hook:HOOK点的名字,对于IP层,就是取上面的五个值;
3)skb:
4)indev:数据包进来的设备,以struct net_device结构表示;
5)outdev:数据包出去的设备,以struct net_device结构表示;
6)okfn:是个函数指针,当所有的该HOOK点的所有登记函数调用完后,根据返回结果决定是否调用此函数
可以看出如果没有配置CONFIG_NETFILTER,则直接调用NF_HOOK最后一个参数说指向的函数,对本例来讲,即直接调用ip_rcv_finish(skb),若配置CONFIG_NETFILTER,则调用NF_HOOK_THRESH,在这个宏中多了一个参数,用来指定回调函数是否能被调用的优先级限制(回调函数在注册时会指定优先级,在以后的章节中会有说明),只有优先级高于该参数时,回调函数才能被调用。在NF_HOOK_THRESH中会调用nf_hook_thresh函数:
static inline int nf_hook_thresh(u_int8_t pf, unsigned int hook,
struct sk_buff *skb,
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_empty(&nf_hooks[pf][hook]))
return 1;
#endif
return nf_hook_slow(pf, hook, skb, indev, outdev, okfn, thresh);
}
在
该函数中又多了一个参数cond,如果它为0,则放弃遍历回调函数,直接运行okfn所指向的函数。若该参数不为0,则调用函数nf_hook_slow():
int nf_hook_slow(u_int8_t pf, unsigned int hook, struct sk_buff *skb,
struct net_device *indev,
struct net_device *outdev,
int (*okfn)(struct sk_buff *),
int hook_thresh)
{
struct list_head *elem;
unsigned int verdict;
int ret = 0;
/* We may 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], skb, hook, indev,
outdev, &elem, okfn, hook_thresh);
if (verdict == NF_ACCEPT || verdict == NF_STOP) {
ret = 1;
} else if (verdict == NF_DROP) {
kfree_skb(skb);
ret = -EPERM;
} else if ((verdict & NF_VERDICT_MASK) == NF_QUEUE) {
if (!nf_queue(skb, elem, pf, hook, indev, outdev, okfn,
verdict >> NF_VERDICT_BITS))
goto next_hook;
}
rcu_read_unlock();
return ret;
}
在该函数中出现了全局变量nf_hooks,它是一个二维数组,其元素为链表头,链表中的元素即为回调函数指针,在这个函数中就实现了遍历指定HOOK节点所挂载的回调函数的功能。在经过回调函数的处理,数据包可能会被丢掉,正常传输等,最终要给上层调用者一个响应结果。回调函数的返回值如下所示:
#define NF_DROP 0
#define NF_ACCEPT 1
#define NF_STOLEN 2
#define NF_QUEUE 3
#define NF_REPEAT 4
#define NF_STOP 5
#define NF_MAX_VERDICT NF_STOP
NF_ACCEPT 继续正常传输数据报。这个返回值告诉Netfilter:到目前为止,该数据包还是被接受的并且该数据包应当被递交到网络协议栈的下一个阶段。NF_DROP 丢弃该数据报,不再传输。
NF_STOLEN 模块接管该数据报,告诉Netfilter“忘掉”该数据报。该回调函数将从此开始对数据包的处理,并且Netfilter应当放弃对该数据包做任何的处理。但是,这并不意味着该数据包的资源已经被释放。这个数据包以及它独自的sk_buff数据结构仍然有效,只是回调函数从Netfilter 获取了该数据包的所有权。
NF_QUEUE 对该数据报进行排队(通常用于将数据报给用户空间的进程进行处理)
NF_REPEAT 再次调用该回调函数,应当谨慎使用这个值,以免造成死循环。
extern struct list_head nf_hooks[NFPROTO_NUMPROTO][NF_MAX_HOOKS];
这个声明详细的描述了nf_hooks,其中NFPROTO_NUMPROTO为Netfilter所支持的协议的数量,NF_MAX_HOOKS为各协议所支持的最大挂载点数。
enum {
NFPROTO_UNSPEC = 0,
NFPROTO_IPV4 = 2,
NFPROTO_ARP = 3,
NFPROTO_BRIDGE = 7,
NFPROTO_IPV6 = 10,
NFPROTO_DECNET = 12,
NFPROTO_NUMPROTO,
};
#define NF_MAX_HOOKS 8
对于IPv4支持的挂载点为:
enum nf_inet_hooks {
NF_INET_PRE_ROUTING,
NF_INET_LOCAL_IN,
NF_INET_FORWARD,
NF_INET_LOCAL_OUT,
NF_INET_POST_ROUTING,
NF_INET_NUMHOOKS
};
- 注册
前面介绍的挂载点函数是通过NF_HOOK宏来实现回调函数的调用的,而调用的过程就是遍历全局变量nf_hooks中相应挂载点的回调函数链表。那么对于回调函数的注册就是填充相应的链表了。
int nf_register_hook(struct nf_hook_ops *reg); //注册一个回调函数int nf_register_hooks(struct nf_hook_ops *reg, unsigned int n); //注册一组回调函数
回调函数的由struct nf_hook_ops来描述:
struct nf_hook_ops
{
struct list_head list; //指向相应HOOK点的回调函数链表表头
nf_hookfn *hook; //回调函数指针
struct module *owner; //表示hook所属模块
u_int8_t pf; //协议簇
unsigned int hooknum; //所要挂载的HOOK点
int priority; //优先级
};
由于在HOOK点不止挂载一个回调函数,对于数据包的处理顺序不同,得到的结果肯定也会有所差别,因此对于挂载在同一个HOOK点的所有回调函数,应该有个排序的准则,而这个准则就是回调函数的优先级
enum nf_ip_hook_priorities {
NF_IP_PRI_FIRST = INT_MIN,
NF_IP_PRI_CONNTRACK_DEFRAG = -400,
NF_IP_PRI_RAW = -300,
NF_IP_PRI_SELINUX_FIRST = -225,
NF_IP_PRI_CONNTRACK = -200,
NF_IP_PRI_MANGLE = -150,
NF_IP_PRI_NAT_DST = -100,
NF_IP_PRI_FILTER = 0,
NF_IP_PRI_SECURITY = 50,
NF_IP_PRI_NAT_SRC = 100,
NF_IP_PRI_SELINUX_LAST = 225,
NF_IP_PRI_CONNTRACK_CONFIRM = INT_MAX,
NF_IP_PRI_LAST = INT_MAX,
};
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);
return 0;
}
通过该注册函数可以看到,在注册回调函数是会根据回调函数的优先级来选择插入链表的位置,优先级越高的回调函数越靠近链表表头,越先得到调用。
5.小结
前面介绍了Netfilter的基本架构,说明了其处理数据的基本流程,以及挂载点回调函数的注册与调用。数据到达后会按照框架的标识的方向流动,而每到一个HOOK点都会检查调用HOOK点的回调函数,HOOK点的回调函数根据自身的优先级进行调用,优先级越高,越先被调用。后面会有Netfilter模块更多的分析。