Netfilter源码分析(4)

2020-05-27 00:00:00 函数 调用 数据包 钩子 注册

三、Hook的注册

如果你对Netfilter的hook的注册还不了解的话,推荐先到网上搜搜《深入Linux网络核心堆栈》bioforge <alkerr@yifan.net>
看看先。(本节中有部份文字引自该文)

注册一个hook函数是围绕nf_hook_ops数据结构的一个非常简单的操作,nf_hook_ops数据结构在linux/netfilter.h中定义,
该数据结构的定义如下:
struct nf_hook_ops
{
struct list_head list;

/* User fills in from here down. */
nf_hookfn *hook;
int pf;
int hooknum;
/* Hooks are ordered in ascending priority. */
int priority;
};

该数据结构中的list成员用于维护Netfilter hook的列表。
hook成员是一个指向nf_hookfn类型的函数的指针,该函数是这个hook被调用时执行的函数。
nf_hookfn同样在linux/netfilter.h中定义。
pf这个成员用于指定协议族。有效的协议族在linux/socket.h中列出,但对于IPv4我们希望使用协议族PF_INET。
hooknum这个成员用于指定安装的这个函数对应的具体的hook类型:
NF_IP_PRE_ROUTING 在完整性校验之后,选路确定之前
NF_IP_LOCAL_IN 在选路确定之后,且数据包的目的是本地主机
NF_IP_FORWARD 目的地是其它主机地数据包
NF_IP_LOCAL_OUT 来自本机进程的数据包在其离开本地主机的过程中
NF_IP_POST_ROUTING 在数据包离开本地主机“上线”之前

后,priority这个成员用于指定在执行的顺序中,这个hook函数应当在被放在什么地方。
对于IPv4,可用的值在linux/netfilter_ipv4.h的nf_ip_hook_priorities枚举中定义。

针对HOOK的注册,在初始化函数中有:
/* Register table */
ret = ipt_register_table(&packet_filter);
if (ret < 0)
return ret;

/* Register hooks */
ret = nf_register_hook(&ipt_ops[0]);
if (ret < 0)
goto cleanup_table;

ret = nf_register_hook(&ipt_ops[1]);
if (ret < 0)
goto cleanup_hook0;

ret = nf_register_hook(&ipt_ops[2]);
if (ret < 0)
goto cleanup_hook1;
可见,注册是通过nf_register_hook函数来完成,每一个Hook的相关信息,都在ipt_ops结构数组中,它的成员变量前面已做分析,
来看看它的初始化值:
static struct nf_hook_ops ipt_ops[]
= { { { NULL, NULL }, ipt_hook, PF_INET, NF_IP_LOCAL_IN, NF_IP_PRI_FILTER },
{ { NULL, NULL }, ipt_hook, PF_INET, NF_IP_FORWARD, NF_IP_PRI_FILTER },
{ { NULL, NULL }, ipt_local_out_hook, PF_INET, NF_IP_LOCAL_OUT,
NF_IP_PRI_FILTER }
};

对应结构各成员变量的含义,可见,filter表上总共设置了NF_IP_LOCAL_IN,NF_IP_FORWARD,NF_IP_LOCAL_OUT,用熟了iptables三个
链,对这三个东东应该是刻骨铭心了。协议簇是PF_INET,初始化链表为NULL,处理函数,前两个为ipt_hook,后一个为ipt_local_out_hook,
优化级均为NF_IP_PRI_FILTER。

hook的注册,是通过nf_register_hook来完成的,它也是一个维护双向链表的过程,值得注意的是,注册的钩子函数,全部是放在全局变量
nf_hooks中,它是一个二维数组,函数一开始先遍历它,找到合适的地方,再将当前节点插入之。(我们可以想像,将来调用钩子函数时,就
是一个查找nf_hooks数组成员的过程)
int nf_register_hook(struct nf_hook_ops *reg)
{
struct list_head *i;

br_write_lock_bh(BR_NETPROTO_LOCK);
/*寻找与当前待注册节点reg匹配的数组元素(按协议族和Hook来匹配)*/
for (i = nf_hooks[reg->pf][reg->hooknum].next;
i != &nf_hooks[reg->pf][reg->hooknum];
i = i->next) {
if (reg->priority < ((struct nf_hook_ops *)i)->priority)
break;
}
/*添加节点*/
list_add(®->list, i->prev);
br_write_unlock_bh(BR_NETPROTO_LOCK);
return 0;
}

能过表的注册,HOOK的注册,准备工作基本上就完成了,其它表的注册和Hook的注册,都是一样的,可以对照分析,没有必要再详述了。
不过注册也只是准备工作。重要的事情是对数据包的处理,对于filter来说,就是包过滤,对于nat来讲,就是地址转换。

四、数据包过滤

1、钩子函数
以中转包过滤为例(FORWARD),注册的时候,向内核注册了一个ipt_hook的钩子函数。
static unsigned int
ipt_hook(unsigned int hook, //Hook类型
struct sk_buff **pskb, //数据包
const struct net_device *in, //进入数据包接口
const struct net_device *out, //离开数据包接口
int (*okfn)(struct sk_buff *)) //默认处理函数
{
return ipt_do_table(pskb, hook, in, out, &packet_filter, NULL);
}

转向到了ipt_do_table。也就是说,如果向内核挂了钩,中转的数据,将进入ipt_do_table函数。

2、钩子函数被调用
钩子函数被注册了,但是内核是如何调用它的呢?
在/src/net/ipv4下边,对应于input/output/forward,分别有Ip_forward.c,Ip_output.c,Ip_input.c。同样继续以forward为例,
(关于linux堆栈处理数据包流程的各个函数的作用等,这里就不进一步详述,请参考其它相关资料)。

对于转发的数据,将进入Ip_forward.c中的ip_forward函数,当处理完成后,在后一句,可以看到:
return NF_HOOK(PF_INET, NF_IP_FORWARD, skb, skb->dev, dev2,
ip_forward_finish);
事实上,你在linux的每一个数据转发的"关节"的函数处,都可以发现这个宏的调用,它就是调用我们注册的钩子,其后一个参数为
下一步处理的函数,即,如果有钩子函数,则处理完所有的钩子函数后,调用这个函数继续处理,如果没有注册任何钩子,则直接调用
此函数。

/* This is gross, but inline doesn't cut it for avoiding the function
call in fast path: gcc doesn't inline (needs value tracking?). --RR */
#ifdef CONFIG_NETFILTER_DEBUG
#define NF_HOOK nf_hook_slow
#else
#define NF_HOOK(pf, hook, skb, indev, outdev, okfn) \
(list_empty(&nf_hooks[(pf)][(hook)]) \
? (okfn)(skb) \
: nf_hook_slow((pf), (hook), (skb), (indev), (outdev), (okfn)))
#endif


先初略看看这个宏,okfn,我们已讲过,它是下一步要处理的函数,这里先调用
list_empty函数检查nf_hooks是否为空,为空则表示没有Hook注册,则直接调用
okfn继续处理。如果不为空,则转入nf_hook_slow函数:


int nf_hook_slow(int pf, unsigned int hook, struct sk_buff *skb,
struct net_device *indev,
struct net_device *outdev,
int (*okfn)(struct sk_buff *))
{
struct list_head *elem;
unsigned int verdict;
int ret = 0;

/* This stopgap cannot be removed until all the hooks are audited. */
if (skb_is_nonlinear(skb) && skb_linearize(skb, GFP_ATOMIC) != 0) {
kfree_skb(skb);
return -ENOMEM;
}
if (skb->ip_summed == CHECKSUM_HW) {
if (outdev == NULL) {
skb->ip_summed = CHECKSUM_NONE;
} else {
skb_checksum_help(skb);
}
}

/* We may already have this, but read-locks nest anyway */
br_read_lock_bh(BR_NETPROTO_LOCK);

#ifdef CONFIG_NETFILTER_DEBUG
if (skb->nf_debug & (1 << hook)) {
printk("nf_hook: hook %i already set.\n", hook);
nf_dump_skb(pf, skb);
}
skb->nf_debug |= (1 << hook);
#endif

/*因为在调用NF_HOOK宏时,已经指定了协议簇和钩子名称,所以要找到对应的Hook点,是很容易的
elem即为我们要找的,记得struct nf_hook_ops结构么?双向链表中的每个elem->hook就是我们关心的终极目标*/
elem = &nf_hooks[pf][hook];
/*找到后,遍历双向链表,进一步处理,以调用Hook函数,并返回相应的动作*/
verdict = nf_iterate(&nf_hooks[pf][hook], &skb, hook, indev,
outdev, &elem, okfn);
if (verdict == NF_QUEUE) {
NFDEBUG("nf_hook: Verdict = QUEUE.\n");
nf_queue(skb, elem, pf, hook, indev, outdev, okfn);
}
/*如果是接受,则调用okfn继续处理,否则丢度之*/
switch (verdict) {
case NF_ACCEPT:
ret = okfn(skb);
break;

case NF_DROP:
kfree_skb(skb);
ret = -EPERM;
break;
}

br_read_unlock_bh(BR_NETPROTO_LOCK);
return ret;
}

再来看nf_iterate:

static unsigned int nf_iterate(struct list_head *head,
struct sk_buff **skb,
int hook,
const struct net_device *indev,
const struct net_device *outdev,
struct list_head **i,
int (*okfn)(struct sk_buff *))
{
/*循环遍历所有注册的钩子函数,包括系统默认的三个,用户自定义的……*/
for (*i = (*i)->next; *i != head; *i = (*i)->next) {
struct nf_hook_ops *elem = (struct nf_hook_ops *)*i;
/*就在这里调用了*/
switch (elem->hook(hook, skb, indev, outdev, okfn)) {
case NF_QUEUE:
return NF_QUEUE;

case NF_STOLEN:
return NF_STOLEN;

case NF_DROP:
return NF_DROP;

case NF_REPEAT:
*i = (*i)->prev;
break;

#ifdef CONFIG_NETFILTER_DEBUG
case NF_ACCEPT:
break;

default:
NFDEBUG("Evil return from %p(%u).\n",
elem->hook, hook);
#endif
}
}
return NF_ACCEPT;
}

解释一下各个返回值:

NF_DROP 丢弃该数据包
NF_ACCEPT 保留该数据包
NF_STOLEN 忘掉该数据包
NF_QUEUE 将该数据包插入到用户空间
NF_REPEAT 再次调用该hook函数

这样,终关心的还是每一个注册的函数,这样又回到本节开头所说的ipt_do_table……

说了这么多,其实只是把简单,没有用的讲了,难的还在于Hook函数,呵呵命运的魔轮已经开启,每一个数据包的命运将会如何?
那就要去分析每一个Hook函数了……

未完,待续……


文章来源CU社区:[原创]Netfilter源码分析-我来抛砖,望能引玉

相关文章