在上一篇文章《netfilter&iptables探讨(1)——基本原理》中,我们分析了netfilter和iptables机制的基本原理以及相关的模块和接口。本文将进一步探讨这套机制的基石——netfilter的原理和实现。
首先需要明确的是,netfilter并不是专为支持iptables而产生的机制,而是Linux内核网络协议栈的公共机制。因此netfilter中内置的hook点其实相当多,在各类网络报文的处理逻辑中都有相对应的hook点,例如IPv4、IPv6、ARP、bridge等。iptables中所使用的是IPv4报文处理路径中的5个hook点,hook点在iptables中被称为链(chain),这可能是因为在每个hook点上都会用链表的形式维护多种iptables规则表的不同处理函数。IPv6的处理路径中也有5个hook点,功能逻辑上和IPv4的是一致的。
下面对netfilter hook点的介绍和图片很大部分来自于《来,今天飞哥带你理解 iptables 原理!》这篇文章,关于IPv4 hook点的更多细节可以在这篇文章中找到。
IPv4 | IPv6 | |
PRE_ROUTING | ip_rcv | ipv6_rcv |
LOCAL_IN | ip_local_deliver | ip6_input |
FORWARD | ip_forward | ip6_forward |
LOCAL_OUT | __ip_local_out | __ip6_local_out/ip6_xmit |
POST_ROUTING | ip_output | ip6_output |
上面的表格中列出了IPv4、IPv6协议栈中的5个hook点名称和位置。
在本地接收报文的过程中,会执行PRE_ROUTING和LOCAL_IN位置的hook函数。如下图所示:
在本地发送报文的过程中,会执行LOCAL_OUT和POST_ROUTING位置的hook函数。如下图所示:
在收到需要转发的报文的处理过程中,会执行PRE_ROUTING、FORWARD和POST_ROUTING位置的hook函数。如下图所示:
把以上这些路径结合起来,就是IP协议栈对报文的主要处理逻辑:
netfilter中注册的hook函数指针定义为:
typedef unsigned int nf_hookfn(void *priv,
struct sk_buff *skb,
const struct nf_hook_state *state);
其中的参数priv是注册hook时提供的私有结构指针。skb是当前处理的数据报文。state则是一些网络上下文信息,包括报文的输入输出网卡、所属的netns、所属的socket信息等。
在hook函数中可以根据网络上下文以及当前的报文内容,对报文进行修改、丢弃、统计、改变后续执行流等各种操作。由于netfilter的使用者都是其他内核模块,本身就有最高权限,因此基于在netfilter hook中可以实现的功能几乎是没有任何限制的。
hook函数的返回值会影响协议栈对报文的后续处理方式,可能的返回值有:
NF_ACCEPT:当前hook函数执行完成,继续执行链表上的下一个hook函数。如果没有下一个hook函数则当前hook点执行完成,返回继续执行协议栈处理流程;
NF_DROP:报文skb需要丢弃,释放掉skb并结束处理流程;
NF_QUEUE:将报文skb加入队列。这里的队列处理逻辑也是可以定制和注册的,但目前只有nfnetlink_queue模块实现了这个接口。通过nfnetlink_queue提供的queue机制,可以将skb加入到一个队列中,并通过netlink协议接口发送给用户态的程序做进一步处理。
NF_STOLEN:报文skb已被hook函数处理完成或接管,直接结束处理流程;
NF_REPEAT:一些文章中说这个返回值会让hook函数被再次调用,在较新的内核上显然并非如此。从手头的5.9.11内核实现来看,hook函数返回NF_ACCEPT、NF_DROP、NF_QUEUE之外的值都会被当做NF_STOLEN处理。NF_REPEAT只会在一些模块的特定逻辑中使用,例如nfnetlink_queue的报文注入逻辑中。这个值不再能作为hook函数返回值使用。
本文在前一篇文章的基础上,进一步分析了netfilter的原理和具体实现。