Netfilter (配合 iptables)使得用户空间应用程序可以注册内核网络栈在处理数据包时应用的处理规则,实现高效的网络转发和过滤。很多常见的主机防火墙程序以及 Kubernetes 的 Service 转发都是通过 iptables 来实现的。
关于 netfilter 的介绍文章大部分只描述了抽象的概念,实际上其内核代码的基本实现不算复杂,本文主要参考 Linux 内核 2.6 版本代码(早期版本较为简单),与最新的 5.x 版本在实现上可能有较大差异,但基本设计变化不大,不影响理解其原理。
本文假设读者已对 TCP/IP 协议有基本了解。
netfilter 的定义是一个工作在 Linux 内核的网络数据包处理框架,为了彻底理解 netfilter 的工作方式,我们首先需要对数据包在 Linux 内核中的处理路径建立基本认识。
数据包在内核中的处理路径,也就是处理网络数据包的内核代码调用链,大体上也可按 TCP/IP 模型分为多个层级,以接收一个 IPv4 的 tcp 数据包为例:
接下来我们正式进入主题。netfilter 的首要组成部分是 netfilter hooks。
对于不同的协议(IPv4、IPv6 或 ARP 等),Linux 内核网络栈会在该协议栈数据包处理路径上的预设位置触发对应的 hook。在不同协议处理流程中的触发点位置以及对应的 hook 名称(蓝色矩形外部的黑体字)如下,本文仅重点关注 IPv4 协议:
所谓的 hook 实质上是代码中的枚举对象(值为从0开始递增的整型):
enum nf_inet_hooks {
NF_INET_PRE_ROUTING,
NF_INET_LOCAL_IN,
NF_INET_FORWARD,
NF_INET_LOCAL_OUT,
NF_INET_POST_ROUTING,
NF_INET_NUMHOOKS
};
每个 hook 在内核网络栈中对应特定的触发点位置,以 IPv4 协议栈为例,有以下 netfilter hooks 定义:
ip_rcv
函数或 IPv6 协议栈的 ipv6_rcv
函数中执行。所有接收数据包到达的第一个 hook 触发点(实际上新版本 Linux 增加了 INGRESS hook 作为最早触发点),在进行路由判断之前执行。ip_local_deliver()
函数或 IPv6 协议栈的 ip6_input()
函数中执行。经过路由判断后,所有目标地址是本机的接收数据包到达此 hook 触发点。ip_forward()
函数或 IPv6 协议栈的 ip6_forward()
函数中执行。经过路由判断后,所有目标地址不是本机的接收数据包到达此 hook 触发点。__ip_local_out()
函数或 IPv6 协议栈的 __ip6_local_out()
函数中执行。所有本机产生的准备发出的数据包,在进入网络栈后首先到达此 hook 触发点。ip_output()
函数或 IPv6 协议栈的 ip6_finish_output2()
函数中执行。本机产生的准备发出的数据包或者转发的数据包,在经过路由判断之后, 将到达此 hook 触发点。所有的触发点位置统一调用 NF_HOOK
这个宏来触发 hook:
static inline int NF_HOOK(uint8_t pf, unsigned int hook, struct sk_buff *skb,
struct net_device *in, struct net_device *out,
int (*okfn)(struct sk_buff *))
{
return NF_HOOK_THRESH(pf, hook, skb, in, out, okfn, INT_MIN);
}
NF-HOOK
接收的参数如下:
NFPROTO_IPV4
。NF-HOOK
的返回值是以下具有特定含义的 netfilter 向量之一:
NF-HOOK
中最后执行传入的 okfn
)。回归到源码,IPv4 内核网络栈会在以下代码模块中调用 NF_HOOK()
:
实际调用方式以 net/ipv4/ip_forward.c
对数据包进行转发的源码为例,在 ip_forward
函数结尾部分的第 115 行以 NF_INET_FORWARD hook 作为入参调用了 NF_HOOK
宏,并将网络栈接下来的处理函数 ip_forward_finish
作为 okfn
参数传入**:**
int ip_forward(struct sk_buff *skb)
{
.....(省略部分代码)
if (rt->rt_flags&RTCF_DOREDIRECT && !opt->srr && !skb_sec_path(skb))
ip_rt_send_redirect(skb);
skb->priority = rt_tos2priority(iph->tos);
return NF_HOOK(NFPROTO_IPV4, NF_INET_FORWARD, skb, skb->dev,
rt->dst.dev, ip_forward_finish);
.....(省略部分代码)
}
netfilter 的另一组成部分是 hook 的回调函数。内核网络栈既使用 hook 来代表特定触发位置,也使用 hook (的整数值)作为数据索引来访问触发点对应的回调函数。
内核的其他模块可以通过 netfilter 提供的 api 向指定的 hook 注册回调函数,同一 hook 可以注册多个回调函数,通过注册时指定的 priority 参数可指定回调函数在执行时的优先级。
注册 hook 的回调函数时,首先需要定义一个 nf_hook_ops
结构(或由多个该结构组成的数组),其定义如下:
struct nf_hook_ops {
struct list_head list;
/* User fills in from here down. */
nf_hookfn *hook;
struct module *owner;
u_int8_t pf;
unsigned int hooknum;
/* Hooks are ordered in ascending priority. */
int priority;
};
在定义中有 3 个重要成员:
NF_HOOK
类似,可通过 okfn
参数嵌套其他函数。定义结构体后可通过 int nf_register_hook(struct nf_hook_ops *reg)
或 int nf_register_hooks(struct nf_hook_ops *reg, unsigned int n);
分别注册一个或多个回调函数。同一 netfilter hook 下所有的 nf_hook_ops
注册后以 priority 为顺序组成一个链表结构,注册过程会根据 priority 从链表中找到合适的位置,然后执行链表插入操作。
在执行 NF-HOOK
宏触发指定的 hook 时,将调用 nf_iterate
函数迭代这个 hook 对应的 nf_hook_ops
链表,并依次调用每一个 nf_hook_ops
的注册函数成员 hookfn
。示意图如下:
这种链式调用回调函数的工作方式,也让 netfilter hook 被称为 Chain,下文的 iptables 介绍中尤其体现了这一关联。
每个回调函数也必须返回一个 netfilter 向量;如果该向量为 NF_ACCEPT,nf_iterate
将会继续调用下一个 nf_hook_ops
的回调函数,直到所有回调函数调用完毕后返回 NF_ACCEPT;如果该向量为 NF_DROP,将中断遍历并直接返回 NF_DROP;如果该向量为 NF_REPEAT,将重新执行该回调函数。 nf_iterate
的返回值也将作为 NF-HOOK
的返回值,网络栈将根据该向量值判断是否继续执行处理函数。示意图如下:
netfilter hook 的回调函数机制具有以下特性:
基于内核 netfilter 提供的 hook 回调函数机制,netfilter 作者 Rusty Russell 还开发了 iptables,实现在用户空间管理应用于数据包的自定义规则。
iptbles 分为两部分:
在内核网络栈中,iptables 通过 xt_table
结构对众多的数据包处理规则进行有序管理,一个 xt_table
对应一个规则表,对应的用户空间概念为 table。不同的规则表有以下特征:
基于规则的最终目的,iptables 默认初始化了 4 个不同的规则表,分别是 raw、 filter、nat 和 mangle。下文以 filter 为例介绍 xt_table
的初始化和调用过程。
filter table 的定义如下:
#define FILTER_VALID_HOOKS ((1 << NF_INET_LOCAL_IN) | \
(1 << NF_INET_FORWARD) | \
(1 << NF_INET_LOCAL_OUT))
static const struct xt_table packet_filter = {
.name = "filter",
.valid_hooks = FILTER_VALID_HOOKS,
.me = THIS_MODULE,
.af = NFPROTO_IPV4,
.priority = NF_IP_PRI_FILTER,
};
(net/ipv4/netfilter/iptable_filter.c)
在 iptable_filter.c 模块的初始化函数 [iptable_filter_init](https://elixir.bootlin.com/linux/v2.6.39.4/C/ident/iptable_filter_init)
****中,调用xt_hook_link
对 xt_table
结构 packet_filter 执行如下初始化过程:
.valid_hooks
属性迭代 xt_table
将生效的每一个 hook,对于 filter 来说是 NF_INET_LOCAL_IN,NF_INET_FORWARD 和 NF_INET_LOCAL_OUT 这3个hook。xt_table
的 priority 属性向 hook 注册一个回调函数。不同 table 的 priority 值如下:
enum nf_ip_hook_priorities {
NF_IP_PRI_RAW = -300,
NF_IP_PRI_MANGLE = -150,
NF_IP_PRI_NAT_DST = -100,
NF_IP_PRI_FILTER = 0,
NF_IP_PRI_SECURITY = 50,
NF_IP_PRI_NAT_SRC = 100,
};
当数据包到达某一 hook 触发点时,会依次执行不同 table 在该 hook 上注册的所有回调函数,这些回调函数总是根据上文的 priority 值以固定的相对顺序执行:
filter 注册的 hook 回调函数 iptable_filter_hook 将对 xt_table
结构执行公共的规则检查函数 ipt_do_table。ipt_do_table
接收 skb
、hook
和 xt_table
作为参数,对 skb
执行后两个参数所确定的规则集,返回 netfilter 向量作为回调函数的返回值。
在深入规则执行过程前,需要先了解规则集如何在内存中表示。每一条规则由 3 部分组成:
作者仅向会员开放了此篇文章。开通会员即可立即解锁此文章和其他会员专属权益。