一 概述
中篇已经提到了钩子函数的注册,也知道最终数据进来是通过钩子函数处理,来实现防火墙的功能的。那么netfilter 内核是在什么时候调用钩子函数?钩子函数又是怎么实现防火墙对应的功能的?(本章主要讲钩子函数实现的过滤功能)
二 调用钩子函数
中篇知道钩子函数最终会注册挂载到 struct net 结构体下的struct netns_nf nf 结构体中,但是钩子函数又是在什么时候调用的呢?
前篇在讲解五链表提到一张图,如下图
其实钩子函数就是在设置的这五个关卡上调用的,而每个关卡(hook)上,调用钩子函数都是调用了netfilter.h中的一个内联函数,NF_HOOK() 函数 或者NF_HOOK_COND()函数,这两函数唯一区别,就是调用NF_HOOK_COND的条件是:如果协议栈当前所处理的数据包skb中没有重新路由的标记,数据包才会进入Netfilter框架。即cond = 1 时进入钩子,否则直接调用okfn函数走协议栈去处理。
static inline int
NF_HOOK_COND(uint8_t pf, unsigned int hook, struct net *net, struct sock *sk,
struct sk_buff *skb, struct net_device *in, struct net_device *out,
int (*okfn)(struct net *, struct sock *, struct sk_buff *),
bool cond)
{
int ret;
if (!cond ||
((ret = nf_hook(pf, hook, net, sk, skb, in, out, okfn)) == 1))
ret = okfn(net, sk, skb);
return ret;
}
static inline int
NF_HOOK(uint8_t pf, unsigned int hook, struct net *net, struct sock *sk, struct sk_buff *skb,
struct net_device *in, struct net_device *out,
int (*okfn)(struct net *, struct sock *, struct sk_buff *))
{
int ret = nf_hook(pf, hook, net, sk, skb, in, out, okfn);
if (ret == 1)
ret = okfn(net, sk, skb);
return ret;
}
NF_HOOK各个参数的解释说明:
· pf:协议族名,Netfilter架构同样可以用于IP层之外,因此这个变量还可以有诸如PF_INET6,PF_DECnet等名字。
· hook:HOOK点的名字,这里指我们讨论的五条链
· net :钩子函数,全部规则都会挂载到struct net 这个结构体上
· sk :套接字结构体
· skb:协议栈的结构体
· in:数据包进来的设备,以struct net_device结构表示;
· out :数据包出去的设备,以struct net_device结构表示;
· okfn :是个函数指针
nf_hook函数
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_entries *hook_head = NULL;
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();
switch (pf) {
case NFPROTO_IPV4:
hook_head = rcu_dereference(net->nf.hooks_ipv4[hook]);
break;
case NFPROTO_IPV6:
hook_head = rcu_dereference(net->nf.hooks_ipv6[hook]);
break;
case NFPROTO_ARP:
#ifdef CONFIG_NETFILTER_FAMILY_ARP
hook_head = rcu_dereference(net->nf.hooks_arp[hook]);
#endif
break;
case NFPROTO_BRIDGE:
#ifdef CONFIG_NETFILTER_FAMILY_BRIDGE
hook_head = rcu_dereference(net->nf.hooks_bridge[hook]);
#endif
break;
#if IS_ENABLED(CONFIG_DECNET)
case NFPROTO_DECNET:
hook_head = rcu_dereference(net->nf.hooks_decnet[hook]);
break;
#endif
default:
WARN_ON_ONCE(1);
break;
}
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, 0);
}
rcu_read_unlock();
return ret;
}
钩子函数是挂载到了struct net 结构体下的struct netns_nf 结构体上了,中篇对struct netns_nf做过讲解。
struct netns_nf {
struct nf_hook_entries __rcu *hooks_ipv4[NF_INET_NUMHOOKS]; // 不同的链对应不同的钩子函数
}
struct nf_hook_entries {
u16 num_hook_entries; //表示这条链注册了多少个钩子函数
/* padding */
struct nf_hook_entry hooks[]; // 对应注册的所有钩子函数数组
}
struct nf_hook_entry {
nf_hookfn *hook;
void *priv;
};
结合着struct netns_nf 结构体,再分析nf_hook函数,前面一大段代码主要是获取对应协议下的指定链表中注册的钩子函数,即代码中的获取的hook_head结构体。 nf_hook_state_init 函数很简单,就是将后面的参数合并到nf_hook_state结构体。
主要看nf_hook_slow 函数,
int nf_hook_slow(struct sk_buff *skb, struct nf_hook_state *state,
const struct nf_hook_entries *e, unsigned int s)
{
unsigned int verdict;
int ret;
for (; s < e->num_hook_entries; s++) {
verdict = nf_hook_entry_hookfn(&e->hooks[s], skb, state);
switch (verdict & NF_VERDICT_MASK) {
case NF_ACCEPT:
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, e, s, verdict);
if (ret == 1)
continue;
return ret;
default:
/* Implicit handling for NF_STOLEN, as well as any other
* non conventional verdicts.
*/
return 0;
}
}
return 1;
}
static inline int
nf_hook_entry_hookfn(const struct nf_hook_entry *entry, struct sk_buff *skb,
struct nf_hook_state *state)
{
return entry->hook(entry->priv, skb, state);
}
nf_hook_slow 函数也很简单,一条链上有多个钩子函数,当数据进到对应链时,就调用链中所有的钩子函数对数据进行处理。同时返回钩子的处理结果,netfilter 决定最后数据还需不需要上抛给协议栈处理。至此钩子函数调用完成。
下面列举出调用NF_HOOk的钩子函数的几个地方
· 首先进入PRE_ROUTING链 ,在 net/ipv4/ip_input.c 文件 ip_rcv 函数中会调用NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,net, NULL, skb, dev, NULL,ip_rcv_finish);
该函数主要用来处理网络层的IP报文的入口函数,它是Netfilter框架的切入点。
当协议栈收到一个NFPROTO_IPV4协议的报文,入口会先去匹配NF_INET_PRE_ROUTING链的钩子函数。钩子函数会将报文与NF_INET_PRE_ROUTING链表下的规则进行比对。最后根据返回值来确定ip_rcv_finish函数的执行情况。
· 接下来根据路由抉择后,所有需要本机转发的报文会进入FORWARD链,在net/ipv4/ip_forward.c 文件 ip_forward 函数会调用NF_HOOK(NFPROTO_IPV4, NF_INET_FORWARD, net, NULL, skb, skb->dev, rt->dst.dev, ip_forward_finish);
· 而要是不需要转发的,数据目的地址是本机时,会进入INPUT链,数据会交给 net/ipv4/ip_input.c 文件中的 ip_local_deliver 函数 ,最终函数会调用NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN, net, NULL, skb, skb->dev, NULL,ip_local_deliver_finish);
· 进入本机处理后的数据,在经过路由抉择后,再从本机发送出去,进入OUTPUT链。在 net/ipv4/ip_output.c中的ip_local_out函数。切入点nf_hook(NFPROTO_IPV4,NF_INET_LOCAL_OUT, net, sk, skb, NULL, skb_dst(skb)->dev, dst_output);
· 最后本机不管是转发的,还是本机发出的,都会经过POST_ROUTING 链,到达net/ipv4/ip_output.c 文件中的ip_mc_output函数 调用NF_HOOK(NFPROTO_IPV4,NF_INET_POST_ROUTING,net, sk, newskb, NULL, newskb->dev,ip_mc_finish_output);
特别注意:
对于钩子函数之前我一直存在个误区,这里不得不提下。 我们说四表五链,每张表都有自己的链表,而规则是挂在相应表的某条链上的。即 filter表中output链 中有条规则,那么这条规则是filter表中output 链独有的。去查看nat表的output链 这条规则肯定是不存在的。
而钩子又是跟链相关的。而我们分析下来一条链是可以有多个钩子函数的。之前我一直认为filter表output 链 可能会注册多个钩子函数,分优先级一个一个执行。而filter表output 链 的钩子函数也是独有的。这是错误的,不知道你看出来了没?
首先一张表的一条链上只可能有一个钩子函数,你没必要注册多个钩子函数,一个一个执行。因为你调用一个钩子函数,然后一步步执行,本质上就达到过滤的效果。
那么代码分析出来看到一条链是可以有多个钩子函数的,这又是怎么回事?其实这里的一条链的多个钩子函数,是指不同表相同链的钩子函数。 即output链 会挂载着filter表的钩子,还有nat的钩子。
你可能又有疑问了。 规则不是对于不同表的相同链是独立特有的吗?而钩子函数对于不同表的相同链确实共用的?其实两则并不冲突
还记得 struct net 结构体有两个变量
· struct netns_nf nf; // 里面挂载着根据不同的链注册的钩子函数
· struct netns_xt xt; // 里面挂载着不同表的规则链
规则是跟表相关的,不同的表里面都维护了自己的链。最终是会挂载在struct netns_xt xt 结构体的。而钩子其实只跟链表相关,是挂载在struct netns_nf nf 上的。
三 filter 过滤功能
已经明白了钩子函数的注册,和钩子函数是在什么时候调用的。接下来就是看钩子函数的实现了,我们知道每条链都会有不同的钩子函数,不同链钩子函数的作用也是不一样的。而且也知道每张表都会有自己的链,例如filter表其实就挂载了INPUT,FORWARD,OUTPUT链。而中篇注册的时候,可以看到filter表中的这三条链都是调用了ipt_do_table 钩子函数。可以说是iptables包过滤功能的核心部分。接下来我们分析一下整个ipt_do_table()函数执行的过程。
unsigned int
ipt_do_table(struct sk_buff *skb,
const struct nf_hook_state *state,
struct xt_table *table)
{
unsigned int hook = state->hook;
static const char nulldevname[IFNAMSIZ] __attribute__((aligned(sizeof(long))));
const struct iphdr *ip;
/* Initializing verdict to NF_DROP keeps gcc happy. */
unsigned int verdict = NF_DROP;
const char *indev, *outdev;
const void *table_base;
struct ipt_entry *e, **jumpstack;
unsigned int stackidx, cpu;
const struct xt_table_info *private;
struct xt_action_param acpar;
unsigned int addend;
.
.
.
e = get_entry(table_base, private->hook_entry[hook]);
do {
const struct xt_entry_target *t;
const struct xt_entry_match *ematch;
struct xt_counters *counter;
WARN_ON(!e);
if (!ip_packet_match(ip, indev, outdev,
&e->ip, acpar.fragoff)) {
no_match:
e = ipt_next_entry(e);
continue;
}
xt_ematch_foreach(ematch, e) {
acpar.match = ematch->u.kernel.match;
acpar.matchinfo = ematch->data;
if (!acpar.match->match(skb, &acpar))
goto no_match;
}
counter = xt_get_this_cpu_counter(&e->counters);
ADD_COUNTER(*counter, skb->len, 1);
t = ipt_get_target_c(e);
WARN_ON(!t->u.kernel.target);
#if IS_ENABLED(CONFIG_NETFILTER_XT_TARGET_TRACE)
/* The packet is traced: log it */
if (unlikely(skb->nf_trace))
trace_packet(state->net, skb, hook, state->in,
state->out, table->name, private, e);
#endif
/* Standard target? */
if (!t->u.kernel.target->target) {
int v;
v = ((struct xt_standard_target *)t)->verdict;
if (v < 0) {
/* Pop from stack? */
if (v != XT_RETURN) {
verdict = (unsigned int)(-v) - 1;
break;
}
if (stackidx == 0) {
e = get_entry(table_base,
private->underflow[hook]);
} else {
e = jumpstack[--stackidx];
e = ipt_next_entry(e);
}
continue;
}
if (table_base + v != ipt_next_entry(e) &&
!(e->ip.flags & IPT_F_GOTO)) {
if (unlikely(stackidx >= private->stacksize)) {
verdict = NF_DROP;
break;
}
jumpstack[stackidx++] = e;
}
e = get_entry(table_base, v);
continue;
}
acpar.target = t->u.kernel.target;
acpar.targinfo = t->data;
verdict = t->u.kernel.target->target(skb, &acpar);
if (verdict == XT_CONTINUE) {
/* Target might have changed stuff. */
ip = ip_hdr(skb);
e = ipt_next_entry(e);
} else {
/* Verdict */
break;
}
} while (!acpar.hotdrop);
xt_write_recseq_end(addend);
local_bh_enable();
if (acpar.hotdrop)
return NF_DROP;
else return verdict;
}
代码比较长,只列出了重要的逻辑部分,主要看do { …} while 循环部分。根据传进来的表xt_table ,紧接着就要获取表中的规则的起始地址。然后用依次按顺序去比较当前正在处理的这个数据包是否和某条规则中的所有过滤项相匹配。如果匹配,就用那条规则里的动作target来处理包,完了之后返回;如果不匹配,当该表中所有的规则都被检查完了之后,该数据包就转入下一个次高优先级的过滤表中去继续执行此操作。依次类推,直到最后包被处理或者被返回到协议栈中继续传输。
四 总结
梳理下netfilter过滤的整个过程:
· 当数据包到达设置的五个关卡(对应五条链),首先根据表的优先级,会先执行优先级高的表
· 每个表会挂载自己关联的链,然后在每条链表上会注册自己的钩子函数。
· 而到了某个关卡(链表),也就会先执行表优先级高的,并且在这条链注册了钩子函数的。
· 进到钩子函数后,对于filter,会先获取表中的规则的起始地址,依次按顺序去比较当前正在处理的这个数据包是否和某条规则中的所有过滤项相匹配。
· 匹配,则会用那条规则里的动作target来处理包
· 要是表中的规则,都与该数据不匹配,则接下来处理下一个优先级的表,这条链的规则。
· 最后要是该关卡(链表),所有的表规则按照优先级都匹配过了,会返回一个状态,netfilter 会根据状态决定该包是否继续协议栈传输。是否到下一个关卡。
至此iptables/ netfilter 防火墙告一段落,有没有觉得还差点东西,防火墙功能其实就讲解了filter过滤功能,就如中篇最后说的,netfilter 还有两个重要的功能,nat地址转换,连接跟踪。其中nat地址转换工作中碰到好多,即使你没看了解过netfilter,也在工作中用到过,重要性不言而喻。而连接跟踪就比较陌生了。连接跟踪是保存连接状态的一种机制。它是状态防火墙和NAT的实现基础,这么一说是不是感觉很高大上?但还是不明白它的具体作用。这里简单说下连接跟踪的一个作用,例如nat地址转换流程,正常的逻辑是一条数据进来,然后到钩子函数,钩子函数然后去匹配所有的规则,最后对匹配的规则进行相应的地址转换。那么问题来了,一个数据包就要遍历所有的规则,再做地址转换。效率是不是很低。其实连接跟踪,当一条连接的第一个数据包通过时查询nat表时,连接跟踪将转换方法保存下来,后续的报文只需要根据连接跟踪里保存的转换方法就可以了。之所以提到netfilter后面的两个功能,是因为后面两个功能也很重要,只是篇幅有限,后续如果有时间研究会再深入。