Netfilter 内核数据包过滤框架
Netfilter是linux内核中的数据包过滤框架,2.4版本及其后的内核包含该框架,该框架使数据包过滤、网络地址转换(NAT)和其他数据包修改功能成为可能。Netfilter框架由之前的ipfwadm和ipchains系统改进并重新设计而来,iptables工具与其紧密关联并依赖其在内核完成相应功能。
Netfilter框架由一组内核中的hook点组成,内核模块可以在网络栈的这些hook点上注册回调函数,当数据包穿过网络栈上相应的hook点时注册的回调函数将被调用,回调函数可以对参数中的数据包做需要的处理并裁决数据包后续的处理方式。
协议族
netfilter是一个通用的数据包过滤框架,支持多种协议族的数据包过滤。本文后续只关注IPV4协议。
ps:本文参考3.10.0-862.el7.x86_64版本内核代码。
本文参考的内核版本中对NFPROTO_INET
看起来没有具体使用,未来版本可能会将对NFPROTO_INET的hook同时生效到IPV4与IPV6两个协议族上,也就是注册一个回调函数同时过滤两个协议族的数据包。从支持的协议族所对应的数字可以看到兼容了include/linux/socket.h
中各个协议族的定义,对开发者更友好,比如NFPROTO_IPV4 == PF_INET
、NFPROTO_IPV6 == PF_INET6
,开发时混用也不会出错。
1 | enum { |
hook点
我没有关注其他协议族的情况,IPV4下共5个hook点,但是hook点的定义比较有趣。比如:
include/uapi/linux/netfilter_ipv4.h
文件中定义有NF_IP_PRE_ROUTING
include/uapi/linux/netfilter.h
文件中定义有NF_INET_PRE_ROUTING
其他几个hook点定义同样值相同。但是netfilter在内核IPV4与IPV6协议栈中调用回调函数时只使用了NF_INET_PRE_ROUTING
这一组。不确定其他协议栈是否也是这5个hook点。
各个hook点调用时机如下:
NF_INET_PRE_ROUTING
刚刚进入网络层的数据包通过此hook点,此时还没有判断式数据包的接收者是本地还是转发给其他host。NF_INET_FORWARD
通过pre_routing后,如果判断数据包应该转发给其他host,则会进一步通过forward点。NF_INET_LOCAL_IN
通过pre_routing点后,如果判断数据包应该由本地接收,则会进一步通过input点。NF_INET_LOCAL_OUT
本地向外发出的数据包会先经过output点。NF_INET_POST_ROUTING
本地向外发出的数据包经过output点后会经过post_routing点,转发的数据包经过forward点后也会经过post_routing点。
1 | enum nf_inet_hooks { |
裁决值
NF_DROP
netfilter框架将丢掉该数据包并释放相关资源。NF_ACCEPT
回调函数放行了该数据包,将继续调用该hook点后续其他回调函数(如果有的话)或进入后续处理流程。NF_STOLEN
数据包由该回调函数负责后续处理(包括资源释放),netfilter及协议栈将不再对该数据包做任何动作。NF_QUEUE
数据包停止其他回调函数调用,由netfilter放入NFQUEUE队列中。参考NFQUEUE 用户态数据包处理。NF_REPEAT
数据包由netfilter再次调用刚刚返回NF_REPEAT
值的回调函数,返回该值需要谨慎避免死循环。NF_STOP
与NF_ACCEPT
类似,但不再调用该hook点后续的任何回调函数(如果有的话)直接进入后续处理流程。
NF_VERDICT_MASK
裁决动作掩码。由于NF_DROP
和NF_QUEUE
在裁决动作之外可能需要额外的信息附带在返回值中,因此使用该掩码过滤掉其他信息只保留裁决动作。NF_VERDICT_FLAG_QUEUE_BYPASS
该flag位用于标记NFQUEUE入队列失败之后是否进行后续处理。如果没有该flag,入队列失败后将直接释放数据包资源。如果包含该flag,入队列失败后将等同于NF_ACCEPT
做后续处理。NF_VERDICT_QMASK
该掩码记录了用于NFQUEUE队列号的bit位。NF_VERDICT_QBITS
回调函数裁决NF_QUEUE
时,返回值右移此尾数以取到NFQUEUE队列号。NF_QUEUE_NR
一个辅助宏,输入NFQUEUE队列号,返回回调函数的合理返回值。NF_DROP_ERR
一个辅助宏,输入NF_DROP
附带的错误值,返回回调函数的合理返回值。
1 | /* Responses from hook functions. */ |
例子
netfilter只能在内核态注册回调函数,因此需要编译为内核模块,参考Linux kernel module 内核模块
这里贴一个例子,在IPV4协议栈的5个hook点都注册回调函数,udp端口包含10086的数据包在NF_INET_POST_ROUTING
或NF_INET_LOCAL_IN
阶段丢掉,udp端口包含10010的数据包在NF_INET_POST_ROUTING
或NF_INET_LOCAL_IN
阶段放入NFQUEUE队列10010中,其他数据包全部放行。这样也可以联动之前NFQUEUE的例子代码。
ps:注意这里要谨慎一些,因为内核IPV4的所有数据包都会经过该回调函数。
1 |
|
1 | obj-m := netfilter_example.o |
1 | [ 614.926634] my init. |
实现原理
注册回调函数
注册回调函数需要nf_hook_ops结构体,里面定义了注册的回调函数、协议族、hook点和优先级(升序排列执行)。
1 | struct nf_hook_ops { |
1 | struct list_head nf_hooks[NFPROTO_NUMPROTO][NF_MAX_HOOKS] __read_mostly; |
所有的回调函数都注册在二维数组全局变量nf_hooks
中。list_head
是linux内核中常用的双向链表结构,这里不关注。数组的第一个维度是注册回调函数的协议族,第二个维度是注册回调函数的hook点,也就是每一个协议族的每一个hook点都是一个双向链表连接的一组回调函数。NF_MAX_HOOKS
值为8,不确定什么协议族有8个hook点。
nf_register_hook
根据协议族和hook点确定nf_hooks
中的链表,遍历链表根据nf_hook_ops中的优先级插入到链表的合适位置。nf_register_hooks
只是nf_register_hook
的循环包装,
注销回调函数时只是将该nf_hook_ops
结构从链表中移出。
执行回调函数
以IPV4协议族收包为例,在确定网络层协议为IPV4协议后,内核进入ip_rcv
函数。经过一系列检查后,最后运行到NF_HOOK,选择NFPROTO_IPV4
协议族、NF_INET_PRE_ROUTING
hook点,同时设定协议栈下一步处理函数ip_rcv_finish
(这个函数将在该hook点回调函数处理完毕并允许下一步逻辑执行时被调用,参考前文裁决值部分。
由NF_HOOK
开始的netfilter框架执行代码贴在这里,可以看到代码简短逻辑清晰,但能够支持内核模块(比如ip_tables模块)通过注册各种回调函数完成复杂功能。netfilter作为内核数据包过滤框架,很好的体现了“提供机制,而不是策略”的设计思想。
1 | /* |
1 | /** |
1 | unsigned int nf_iterate(struct list_head *head, |