NETFILTER
-
NETFILTER的使用
"栈"模式底层机制基本就是像下面这个样子:
对于收到的每个数据包,都从"A"点进来,经过路由判决,如果是发送给本机的就经过"B"点,然后往协议栈的上层继续传递;否则,如果该数据包的目的地是不本机(数据包的目标L3不是本机IP),那么就经过"C"点,然后顺着"E"点将该包转发出去。
对于发送的每个数据包,首先也有一个路由判决,以确定该包是从哪个接口出去,然后经过"D"点,最后也是顺着"E"点将该包发送出去。
协议栈那五个关键点A,B,C,D和E就是我们Netfilter大展拳脚的地方了。
显然从A点进来的数据包马上就要进入到路由子系统了,所以这些数据包的L2目标肯定是本机的地址,但是L3目标则不一定。
-
钩子类型
Hook 调用的时机
- NF_INET_PRE_ROUTING 在完整性校验之后,选路确定之前
- NF_INET_LOCAL_IN 在选路确定之后,且数据包的目的是本地主机
- NF_INET_FORWARD 目的地是其它主机地数据包
- NF_INET_LOCAL_OUT 来自本机进程的数据包在其离开本地主机的过程中,在本机路由以后。
- NF_INET_POST_ROUTING 在数据包离开本地主机"上线"之前
-
钩子函数
-
数据结构
注册一个hook函数是围绕nf_hook_ops数据结构的一个非常简单的操作,nf_hook_ops数据结构在linux/netfilter.h中定义,该数据结构的定义如下:
struct nf_hook_ops {
struct list_head list;
/* 此下的值由用户填充 */
nf_hookfn *hook; /* hook 函数*/
int pf; /* 协议族编号*/
int hooknum;/*Hook 点的编号*/
int priority;/* Hook以升序的优先级排序 */
};
该数据结构中的list成员用于维护Netfilter hook的列表,并且不是用户在注册hook时需要关心的重点。
hook成员是一个指向nf_hookfn类型的函数的指针,该函数是这个hook被调用时执行的函数。nf_hookfn同样在linux/netfilter.h中定义。
pf这个成员用于指定协议族。有效的协议族在linux/socket.h中列出,但对于IPv4我们希望使用协议族PF_INET。
hooknum这个成员用于指定安装的这个函数对应的具体的hook类型,其值为NF_INET_PRE_ROUTING、NF_INET_LOCAL_IN等,
priority这个成员用于指定在执行的顺序中,这个hook函数应当在被放在什么地方。对于IPv4,可用的值在linux/netfilter_ipv4.h的nf_ip_hook_priorities枚举中定义。
显然pf和hooknum就可以构成一个二维数组,也就是说INET有自己的5个钩子,ARP也有自己的5个钩子。每种协议的某一种钩子的函数链表是完全独立的。
-
hook函数原型
unsigned int nf_hookfn(unsigned int hooknum, struct sk_buff **skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *));
hooknum:用于指定节1.1中给出的hook类型中的一个。
skb:待处理的数据包
in:用于描述数据包到达的接口
out:用于描述数据包离开的接口。
必须明白,在通常情况下,这两个参数中将只有一个被提供。例如:参数in只用于NF_IP_PRE_ROUTING和NF_IP_LOCAL_IN ,参数out只用于NF_IP_LOCAL_OUT和NF_IP_POST_ROUTING。
okfn函数指针,该函数以一个sk_buff数据结构作为它唯一的参数,并且返回一个整型的值。作用???
这个结构中就没有pf了,也就是协议字段没有了。
-
钩子函数的返回类型
NF_DROP(0) 数据包被丢弃。即不被下一个钩子函数处理,同时也不再被协议栈处理,并释放掉该数据包。协议栈将处理下一个数据包。
NF_ACCEPT(1) 数据包允许通过。即交给下一个钩子函数处理、或交给协议栈继续处理(okfn())。
NF_STOLEN(2) 数据包被停止处理。即不被下一个钩子函数处理,同时也不被协议栈处理,但也不释放数据包。协议栈将处理下一个数据包。模块接管该数据报,告诉Netfilter"忘掉"该数据报。该回调函数将从此开始对数据包的处理,并且Netfilter应当放弃对该数据包做任何的处理。但是,这并不意味着该数据包的资源已经被释放。这个数据包以及它独自的sk_buff数据结构仍然有效,只是回调函数从Netfilter 获取了该数据包的所有权。
NF_QUEUE(3) 将数据包交给nf_queue子系统处理。即不被下一个钩子函数处理,同时也不被协议栈处理,但也不释放数据包。协议栈将处理下一个数据包。
NF_REPEAT(4) 数据包将被该返回值的钩子函数再次处理一遍。
NF_STOP(5) 数据包停止被该HOOK点的后续钩子函数处理,并交给协议栈继续处理(okfn())
NF_ACCEPT和NF_STOP都是将数据包交给协议栈了,只是前者还会执行钩子函数链表的写一个处理函数,而后者就会跳过后续的钩子函数链表,直接到达协议栈。NF_ACCEPT只表示本处理函数同意将数据包交由协议栈,但是数据包到不到的了协议栈还要看链表后面的处理函数同意不同意。
NF_STOLEN和NF_STOP相同的地方在于都不会执行钩子函数链表后面的函数,只是NF_STOLEN不会执行函数参数中(okfn()),而NF_STOP则会执行这个函数。见2.3.3的代码。
-
NETFILTER的内幕
-
nf_hooks
-
数据结构
-
struct list_head nf_hooks[NFPROTO_NUMPROTO][NF_MAX_HOOKS]
其中
enum {
NFPROTO_UNSPEC = 0,
NFPROTO_IPV4 = 2,
NFPROTO_ARP = 3,
NFPROTO_BRIDGE = 7,
NFPROTO_IPV6 = 10,
NFPROTO_DECNET = 12,
NFPROTO_NUMPROTO,
};
这个二维数组的每一项代表了一个钩子被调用的点,NF_PROTO代表协议栈,NF_HOOK 代表协议栈中某个路径点。不是所有的协议都会有定义,貌似也可以直接添加。但是如果添加一种协议,那协议栈也得修改。否则NF_HOOK宏怎么添加进去呢。
首先把这个二维数组想像成一个平面。这个平面的长表示协议族(IPV4、IPV6、ARP),平面的宽表示1.1节中的5个钩子。这个平面上的每个格子就是一个list_head,表示每个格子都可以串一串处理函数。也就是说对于IPV4的NF_INET_LOCAL_IN这个钩子可以有多个钩子函数,这些钩子函数按照优先级排列,数据包到来后每个钩子都会执行一遍。
-
初始化
在Core.c (linux-2.6.32-279.el6\net\netfilter)中有如下的定义
void __init netfilter_init(void)
{
int i, h;
for (i = 0; i < ARRAY_SIZE(nf_hooks); i++) {
for (h = 0; h < NF_MAX_HOOKS; h++)
INIT_LIST_HEAD(&nf_hooks[i][h]);
}
}
很简单就是将每个list_head初始化
-
管理钩子
用户通过 nf_register_hook()和 nf_unregister_hook()在这个全局链表中添加或删除HOOK 点。并且在协议栈中会通过NF_HOOK()->nf_hook_slow()来调用这些hook点。
钩子函数的返回值NF_STOP,可以跳过list后面挂接的那些钩子处理函数。而NF_ACCEPT则是交由下一个钩子处理函数(优先级低一点点的那个函数)。
-
nf_register_hook
int nf_register_hook(struct nf_hook_ops *reg) { err = mutex_lock_interruptible(&nf_hook_mutex); 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; }
将参数描述的reg注册到nf_hooks数组中。reg参数的内容可能如下所示:
reg->hook = hook_func;
reg->hooknum = NF_INET_PRE_ROUTING;
reg->pf = PF_INET;
reg->priority = NF_IP_PRI_FIRST;
- 利用mutex_lock_interruptible来加锁
- 按照2.1节中想象出来的那个平面,按照协议类型和钩子类型找到一个格子,这个格子就是一个list_head,然后遍历这个钩子。如果参数中的优先级值小于当前钩子(遍历过程中)的优先级值,则退出循环,然后将这个参数的钩子加入到list中去。
- 解锁。
感觉每个list里面的nf_hook_ops都是按照优先级值从小到大排列,所以实际上优先级值小的优先级要高。--越小越高。
-
nf_unregister_hook
void nf_unregister_hook(struct nf_hook_ops *reg)
{
mutex_lock(&nf_hook_mutex);
list_del_rcu(®->list);
mutex_unlock(&nf_hook_mutex);
synchronize_net();
}
将钩子从list中移去。
-
数据处理
-
NF_HOOK
netfilter在不同协议栈的不同点上(例如arp_rcv()、 ip_rcv()、 ip6_rcv()、 br_forward()等)放置NF_HOOK()函数,当数据包经过了某个协议栈(NF_PROTO)的某个点(NF_HOOK)时,该协议栈会通过NF_HOOK()函数调用对应钩子链表(nf_hooks[NF_PROTO][NF_HOOK])中注册的每一个钩子项来处理该数据包。
在ip_rcv中有如下的代码:
return NF_HOOK(PF_INET, NF_IP_PRE_ROUTING, skb, dev, NULL,
ip_rcv_finish);
每当一个数据包被ip_rcv处理的时候都会调用这个NF_HOOK,其中ip_rcv_finish就是在1.2.2中提到的int (*okfn)(struct sk_buff *))。
#define NF_HOOK(pf, hook, skb, indev, outdev, okfn) \
NF_HOOK_THRESH(pf, hook, skb, indev, outdev, okfn, INT_MIN)
以INT_MIN为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;})
最后那个1是个开关,在nf_hook_thresh中对应着变量cond。nf_hook_thresh中但cond为0时任何事情都不回去干的。所以这个宏会无条件的执行nf_hook_thresh的逻辑。
当nf_hook_thresh返回1才会去执行钩子后面的那个函数(okfn)。(不是下一个钩子处理函数)
#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;})
根据参数cond来决定是否执行nf_hook_thresh的逻辑。
两个地方都用了INT_MIN做阈值,为什么。
有个疑问,既然okfn在这两个宏中马上就要被调用了,那为什么还要把它传给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; // 直接去协议栈了,因为在的调用者NF_HOOK_COND中,如果nf_hook_thresh返回1就会执行那个okfn。
return nf_hook_slow(pf, hook, skb, indev, outdev, okfn, thresh);
}
-
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;// 是为了让NF_HOOK_THRESH能调用okfn,okfn不是钩子处理函数 } else if ((verdict & NF_VERDICT_MASK) == NF_DROP) { kfree_skb(skb);// 释放掉该skb,是框架释放的,所以钩子函数不能释放。 ret = -(verdict >> NF_VERDICT_BITS); if (ret == 0) 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_iterate
unsigned int nf_iterate(struct list_head *head, struct sk_buff *skb, unsigned int hook, const struct net_device *indev, const struct net_device *outdev, struct list_head **i, int (*okfn)(struct sk_buff *), int hook_thresh) //hook_thresh是一个最小最小的整数,所有的数都比它大。 { unsigned int verdict; /* * The caller must not block between calls to this * function because of risk of continuing from deleted element. */ list_for_each_continue_rcu(*i, head) { struct nf_hook_ops *elem = (struct nf_hook_ops *)*i;// 取一个钩子处理函数,优先级大的先取。 if (hook_thresh > elem->priority)// hook_thrsh应该是个阈值,比当前钩子的优先级低就跳过?hook_thresh传进来是INT的最小值,所以这个判断永远不会成功。既然钩子都是按照优先级由高到低排列,用break不更好? 钩子函数的优先级大于阈值的就会被跳过???? continue; verdict = elem->hook(hook, skb, indev, outdev, okfn); // 执行真正的钩子函数, okfn不是钩子。有个疑问,如果钩子函数里面将okfn执行一遍,在NF_HOOK_THRESH中还会执行一遍? 在ip_conntrace里面,处理NF_INET_PRE_ROUTING钩子的函数-- ipv4_conntrack_in就直接把okfn扔掉了 if (verdict != NF_ACCEPT) { // 进入的条件是"不通过" if (verdict != NF_REPEAT)// 进入的条件是"非重入" return verdict;// hook返回的不是NF_ACCEPT,因而没有必要继续了,直接返回。 *i = (*i)->prev;// 很显然,如果是NF_REPEAT,这表示这个hook还需要再执行一遍,所以遍历的指针要回退到上一步也就是指向上一个钩子结构体,然后list_for_each_continue_rcu后,指针则又指向当前的这个钩子结构体。于是当前的钩子结构体将又执行一遍。 } } return NF_ACCEPT; }
根据nf_iterate()返回,会有以下情况:
1.如果结果为NF_ACCEPT,表示勾子函数允许报文继续向下处理,此时应该继续执行队列上的下一个勾子函数,因为这些勾子函数都是对同一类报文在相同位置的过滤,前一个通后,并不能返回,而要所有函数都执行完,结果仍为NF_ACCEPT时,则可返回它;
2.如果结果为NF_REPEAT,表示要重复执行勾子函数一次;所以勾子函数要编写得当,否则报文会一直执行一个返回NF_REPEAET的勾子函数,当返回值为NF_REPEAT时,不会返回;
3.如果为其它结果,则不必再执行队列上的其它函数,直接返回它;如NF_STOP表示停止执行队列上的勾子函数,直接返回;NF_DROP表示丢弃掉报文;NF_STOLEN表示报文不再往上传递,与NF_DROP不同的是,它没有调用kfree_skb()释放掉skb;NF_QUEUE检查给定协议(pf)是否有队列处理函数,有则进行处理,否则丢掉。
-
总结
所谓的注册一个钩子,就是在netfilter里面定义好的一个格子里面(list_head)按照优先级的循序添加一个数据结构,这个数据结构包含一个处理函数。
netfilter在系统的一些关键处已经用NF_HOOK安插了很多桩,这些桩其实和上面的格子是一一对应的。
但数据包经过这些桩的时候会按照优先级来依次调用注册在这个格子的钩子处理函数。然后只有所有的钩子处理函数都放行了,这个数据包才会回到协议栈。可能会被其中的某个钩子处理函数steal,那么后面的钩子处理函数就看不到它了。