CXD Linux Engineer

Linux协议栈--Netfilter源码分析

2017-08-23

netfilter钩子的注册

netfilter框架提供了两个接口用于外部模块注册钩子函数:nf_register_hooknf_register_hooks后者可以同时注册多个钩子函数,两个函数都在/net/netfilter/core.c文件中实现。nf_register_hook函数加锁后调用_nf_register_hook函数,在此函数中遍历网络命名空间链表net_namespace_list,然后调用nf_register_net_hook函数完成实际的注册任务,最终结果是钩子函数会在每个网络命名空间中都注册一遍。关于网络命名空间这个概念可以参考这篇博文Linux Network Namespaces。所有钩子函数在内核中的组织方式如下图所示:
netfilter

按照上面的图来分析一下nf_register_net_hook函数注册钩子的具体实现过程,首先调用nf_hook_entry_head函数根据网络类型和挂载点在网络命名空间的hooks二维数组中找到nf_hook_entry链表。然后初始化一个新的节点,根据优先级将新的节点插入到链表中的对应位置。

int nf_register_net_hook(struct net *net, const struct nf_hook_ops *reg)
{
	struct nf_hook_entry __rcu **pp;
	struct nf_hook_entry *entry, *p;

	if (reg->pf == NFPROTO_NETDEV) {
#ifndef CONFIG_NETFILTER_INGRESS
		if (reg->hooknum == NF_NETDEV_INGRESS)
			return -EOPNOTSUPP;
#endif
		if (reg->hooknum != NF_NETDEV_INGRESS ||
		    !reg->dev || dev_net(reg->dev) != net)
			return -EINVAL;
	}

	/* 查找hooks二维数组中对应的nf_hook_entry链表 */
	pp = nf_hook_entry_head(net, reg);
	if (!pp)
		return -EINVAL;

	/* 分配一个新的 nf_hook_entry 节点并初始化 */
	entry = kmalloc(sizeof(*entry), GFP_KERNEL);
	if (!entry)
		return -ENOMEM;

	nf_hook_entry_init(entry, reg);

	mutex_lock(&nf_hook_mutex);

	/* Find the spot in the list */
	/* 在nf_hook_entry链表中按照优先级顺序,查找新增节点应该放置的位置 */
	for (; (p = nf_entry_dereference(*pp)) != NULL; pp = &p->next) {
		if (reg->priority < nf_hook_entry_priority(p))
			break;
	}
	/* 将新的 nf_hook_entry 节点插入到链表中的指定位置 */
	rcu_assign_pointer(entry->next, p);
	rcu_assign_pointer(*pp, entry);

	mutex_unlock(&nf_hook_mutex);
#ifdef CONFIG_NETFILTER_INGRESS
	if (reg->pf == NFPROTO_NETDEV && reg->hooknum == NF_NETDEV_INGRESS)
		net_inc_ingress_queue();
#endif
#ifdef HAVE_JUMP_LABEL
	static_key_slow_inc(&nf_hooks_needed[reg->pf][reg->hooknum]);
#endif
	return 0;
}

钩子函数的执行

在内核网络协议栈中经常会看到下面这种代码,这就是内核在协议栈中安装的钩子,协议栈中的数据包通过这些钩子进入到Netfilter架构中,然后Netfilter架构调用挂载在此钩子上的处理函数对数据包进行处理。

return NF_HOOK(NFPROTO_IPV4, NF_INET_FORWARD,
		       net, NULL, skb, skb->dev, rt->dst.dev,
		       ip_forward_finish);

当在协议栈中调用NF_HOOK这个内联函数时,最终会在/include/linux/netfilter.h文件中的nf_hook函数中完成实际操作。此函数首先通过协议类型和挂载点找到对应的nf_hook_entry链表,然后调用nf_hook_slow函数依次调用此链表中的钩子函数:

static inline int nf_hook(u_int8_t pf, unsigned int hook, struct net *net,
			  struct sock *sk, struct sk_buff *skb,
			  struct net_device *indev, struct net_device *outdev,
			  int (*okfn)(struct net *, struct sock *, struct sk_buff *))
{
	struct nf_hook_entry *hook_head;
	int ret = 1;

#ifdef HAVE_JUMP_LABEL
	if (__builtin_constant_p(pf) &&
	    __builtin_constant_p(hook) &&
	    !static_key_false(&nf_hooks_needed[pf][hook]))
		return 1;
#endif

	/* 加锁 */
	rcu_read_lock();
	/* 通过协议号 pf 和挂载点 hook 在二维数组中获取 nf_hook_entry 链表 */
	hook_head = rcu_dereference(net->nf.hooks[pf][hook]);
	if (hook_head) {
		struct nf_hook_state state;

		nf_hook_state_init(&state, hook, pf, indev, outdev,
				   sk, net, okfn);

		/* 在此函数中调用实际的钩子函数 */
		ret = nf_hook_slow(skb, &state, hook_head);
	}
	rcu_read_unlock();

	return ret;
}

nf_hook_slow函数会遍历整个链表,然后依次调用节点上的钩子函数,判断钩子函数的返回值,如果钩子函数返回NF_ACCEPT则继续遍历下一个节点;如果返回NF_DROP则释放数据包内存结束遍历直接返回;如果返回NF_QUEUE则将数据包加入到缓存队列中,结束遍历直接返回;其他返回值不做任何处理(例如NF_STOLEN)结束遍历直接返回。

int nf_hook_slow(struct sk_buff *skb, struct nf_hook_state *state,
		 struct nf_hook_entry *entry)
{
	unsigned int verdict;
	int ret;

	/* 使用do while循环遍历链表 */
	do {
		/* 调用节点中的钩子函数 */
		verdict = nf_hook_entry_hookfn(entry, skb, state);
		/* 判断钩子函数的返回值 */
		switch (verdict & NF_VERDICT_MASK) {
		/* 继续遍历下一个节点 */
		case NF_ACCEPT: 
			entry = rcu_dereference(entry->next);
			break;
		/* 丢弃数据包,直接返回 */
		case NF_DROP:
			kfree_skb(skb);
			ret = NF_DROP_GETERR(verdict);
			if (ret == 0)
				ret = -EPERM;
			return ret;
		/* 将数据包入队,直接返回 */
		case NF_QUEUE:
			ret = nf_queue(skb, state, &entry, verdict);
			if (ret == 1 && entry)
				continue;
			return ret;
		default:
			/* Implicit handling for NF_STOLEN, as well as any other
			 * non conventional verdicts.
			 */
			return 0;
		}
	} while (entry);

	return 1;
}

参考

Linux Network Namespaces
基于Linux-4.12.1内核源码分析


Comments

Content