Netfilter (配合 iptables)使得用户空间应用程序可以注册内核网络栈在处理数据包时应用的处理规则,实现高效的网络转发和过滤。很多常见的主机防火墙程序以及 Kubernetes 的 Service 转发都是通过 iptables 来实现的。
关于 netfilter 的介绍文章大部分只描述了抽象的概念,实际上其内核代码的基本实现不算复杂,本文主要参考 Linux 内核 2.6 版本代码(早期版本较为简单),与最新的 5.x 版本在实现上可能有较大差异,但基本设计变化不大,不影响理解其原理。
本文假设读者已对 TCP/IP 协议有基本了解。
netfilter 的定义是一个工作在 Linux 内核的网络数据包处理框架,为了彻底理解 netfilter 的工作方式,我们首先需要对数据包在 Linux 内核中的处理路径建立基本认识。
数据包在内核中的处理路径,也就是处理网络数据包的内核代码调用链,大体上也可按 TCP/IP 模型分为多个层级,以接收一个 IPv4 的 tcp 数据包为例:
network-path
接下来我们正式进入主题。netfilter 的首要组成部分是 netfilter hooks。
对于不同的协议(IPv4、IPv6 或 ARP 等),Linux 内核网络栈会在该协议栈数据包处理路径上的预设位置触发对应的 hook。在不同协议处理流程中的触发点位置以及对应的 hook 名称(蓝色矩形外部的黑体字)如下,本文仅重点关注 IPv4 协议:
netfilter-flow
所谓的 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 定义:
netfilter-hooks-stack
所有的触发点位置统一调用 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 接收的参数如下:
NF-HOOK 的返回值是以下具有特定含义的 netfilter 向量之一:
回归到源码,IPv4 内核网络栈会在以下代码模块中调用 NF_HOOK():
NF_HOOK
实际调用方式以 net/ipv4/ip_forward.c
[1] 对数据包进行转发的源码为例,在 ip_forward 函数结尾部分的第 115 行以 NF_INET_FORWARDhook 作为入参调用了 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 个重要成员:
定义结构体后可通过 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-hookfn1
这种链式调用回调函数的工作方式,也让 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-hookfn2
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[2] 模块的初始化函数 iptable_filter_init ****中,调用xt_hook_link 对 xt_table 结构 packet_filter 执行如下初始化过程:
不同 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 值以固定的相对顺序执行:
tables-priority
filter 注册的 hook 回调函数 iptable_filter_hook[3] 将对 xt_table 结构执行公共的规则检查函数 ipt_do_table[4]。ipt_do_table 接收 skb、hook 和 xt_table作为参数,对 skb 执行后两个参数所确定的规则集,返回 netfilter 向量作为回调函数的返回值。
在深入规则执行过程前,需要先了解规则集如何在内存中表示。每一条规则由 3 部分组成:
ipt_entry 结构体定义如下:
struct ipt_entry { struct ipt_ip ip; unsigned int nfcache; /* ipt_entry + matches 在内存中的大小*/ u_int16_t target_offset; /* ipt_entry + matches + target 在内存中的大小 */ u_int16_t next_offset; /* 跳转后指向前一规则 */ unsigned int comefrom; /* 数据包计数器 */ struct xt_counters counters; /* 长度为0数组的特殊用法,作为 match 的内存地址 */ unsigned char elems[0]; };
ipt_do_table 首先根据 hook 类型以及 xt_table.private.entries属性跳转到对应的规则集内存区域,执行如下过程:
ipt_do_table
以上数据结构与执行方式为 iptables 提供了强大的扩展能力,我们可以灵活地自定义每条规则的匹配条件并根据结果执行不同行为,甚至还能在额外的规则集之间栈式跳转。
由于每条规则长度不等、内部结构复杂,且同一规则集位于连续的内存空间,iptables 使用全量替换的方式来更新规则,这使得我们能够从用户空间以原子操作来添加/删除规则,但非增量式的规则更新会在规则数量级较大时带来严重的性能问题:假如在一个大规模 Kubernetes 集群中使用 iptables 方式实现 Service,当 service 数量较多时,哪怕更新一个 service 也会整体修改 iptables 规则表。全量提交的过程会 kernel lock 进行保护,因此会有很大的更新时延。
用户空间的 iptables 命令行可以读取指定表的数据并渲染到终端,添加新的规则(实际上是替换整个 table 的规则表)等。
iptables 主要操作以下几种对象:
基于上文介绍的代码调用过程流程,chain 和 rule 按如下示意图执行:
iptables-chains
对于 iptables 具体的用法和指令本文不做详细介绍。
仅仅通过 3、4 层的首部信息对数据包进行过滤是不够的,有时候还需要进一步考虑连接的状态。netfilter 通过另一内置模块 conntrack 进行连接跟踪(connection tracking),以提供根据连接过滤、地址转换(NAT)等更进阶的网络过滤功能。由于需要对连接状态进行判断,conntrack 在整体机制相同的基础上,又针对协议特点有单独的实现。
全部0条评论
快来发表一下你的评论吧 !