参考书:《Linux内核模块开发技术指南》
1.Netfilter原理
当一个数据包进入Linux网络协议栈后,Linux网络协议栈会对数据包进行一系列的处理,在这些处理过程中,会有一些钩子点,这些钩子点暴露给模块开发者,Linux开发人员可截获数据包并调用相应接口对数据包进行一些额外处理,例如根据条件丢弃或接收数据包(包过滤)、更改数据包内容(DNAT或SNAT)等。这种基于钩子点的数据包处理框架被称为Netfilter,Linux的防火墙Iptables就是基于Netfilter实现。
在Linux 5.10及以上内核中,Netfilter不仅仅局限于过滤IP数据包,同样可以过滤ARP数据包、DECnet数据包、穿过网桥的数据包等多种协议和路径。由于IPv4数据包的过滤最为常用,本文主要以IPv4数据包的过滤来描述Netfilter原理。
终端在收到IP数据包后,会将数据包交给网络层处理。数据包进入网络层后,会进行路由操作。如果数据包的目的地址是本终端,则数据包会交给上层协议处理;否则,将根据路由表转发数据包到对应终端或路由器。
数据包进入网络层在进行路由操作之前,Linux网络协议栈会对其进行一些处理,这里用PREROUTING(路由前)来表示在路由操作前的处理过程;在查到路由表之后,数据包可能进入传输层,也可能被转发,数据包进入传输层之前,也会进行一些处理,这些处理用INPUT(传送给传输层)来表示;数据包在被转发的时候同样会进行一些处理,用FORWARD(转发)来表示。
如果是终端从应用层发送数据包到其他终端,在数据包进入网络层之后也会对其进行处理,用OUTPUT(发送)来表示;无论是需要转发的数据包还是终端本身发出的数据包,在网络层处理完成后,数据将会传给数据链路层处理,在此之前Linux会对数据包进行POSTROUTING(路由后)处理。整个处理过程下图所示。

图中箭头的方向表示可能的数据流向。在收到数据包后,数据链路层处理完成后将数据交给Linux网络层处理,Linux首先对其进行PREROUTING处理,之后进行路由操作。如果数据发送给本终端,则进行INPUT处理后将数据包交给传输层处理;如果数据包要被转发,则首先进行FORWARD处理,然后在进行POSTROUING处理后将数据交给数据链路层。
终端应用发送数据包时,应用数据经传输层传给网络层处理,Linux在网络层首先对其进行OUTPUT处理,再进行POSTROUTING处理,完成后将数据交给数据链路层。
INPUT、OUTPUT、FORWARD、PREROUTING、POSTROUTING被称为Netfilter的五条链。在这些链上,开发人员可以挂上自定义的函数对网络数据包进行处理,这些由开发人员自定义的函数被称为钩子函数。
2.实现最简单的Netfilter模块
2.1 实现步骤
下面将实现一个自定义钩子函数,该函数的功能是丢弃所有IP数据包。该函数会挂在INPUT链上。实现该功能后,所有进入终端的IP数据包将会被丢弃,其他终端将再也不能访问这台终端。要实现该功能,需要进行如下操作:
(1)自定义一个钩子函数,该函数的功能是丢弃所有数据包
钩子函数的类型为nf_hookfn,该类型定义在内核源码的include/linux/netfilter.h头文件中,定义如下。
typedef unsigned int nf_hookfn(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)
函数的返回值是对数据包的处理结果,即是否丢弃或接收数据包。内核定义了一组宏来表示网络协议栈对数据包的处理结果,这些值定义在内核源码的include/uapi/linux/netfilter.h文件中,如下所示:
#define NF_DROP 0 //丢弃数据包
#define NF_ACCEPT 1 //接收数据包,继续下一个钩子函数的处理
//将数据包从Linux网络协议栈中移除,网络协议栈不再处理该数据包
#define NF_STOLEN 2
//将数据包传给应用程序处理,前提是应用程序会接收并处理NF_QUEUE传递过来的数据包
#define NF_QUEUE 3
#define NF_REPEAT 4 //再次执行同样的钩子函数
//效果类似于NF_ACCEPT,但是不会继续下一个钩子函数的处理,而是直接进入下一个环节
#define NF_STOP 5
#define NF_MAX_VERDICT NF_STOP
对于上述处理结果,最好理解的就是NF_DROP和NF_ACCEPT。如果函数返回NF_DROP,表示丢弃数据包,Linux内核得到该返回值后会将数据包丢弃;NF_ACCEPT是接收数据包,Linux内核得到返回值NF_ACCEPT后会将数据包交给下一个流程处理。对于其他处理结果,将在使用时再做描述。
除了返回值外,nf_hookfn函数的第一个参数priv为私有数据信息,该值可由开发人员自定义并被传入函数;第二个参数skb为网络数据包,该参数的结构体类型struct sk_buff是网络数据包的核心结构体,保存了数据包中的数据和属性信息;第三个参数state是Netfilter钩子点的状态信息,包括钩子点的协议、挂载链、进接口(从哪个网卡进入)、出接口(将从哪个网卡发送)、网络命名空间等信息,结构体定义在内核源码的include/linux/netfilter.h文件中,如下所示:
struct nf_hook_state {
unsigned int hook; //该钩子在哪条链上
u_int8_t pf; //协议信息
struct net_device *in; //入网络接口
struct net_device *out; //出网络接口
struct sock *sk; //套接字信息
struct net *net; //网络命名空间
//如果所有钩子函数都顺利通过,最后执行的函数
int (*okfn)(struct net *, struct sock *, struct sk_buff *);
};
该结构体的各成员变量解释如下。
- hook:该钩子函数挂在哪条链上。对于IPv4数据包过滤,共有五条链:INPUT、OUTPUT、FORWARD、PREROUTING、POSTROUTING。这些链定义在内核源码的include/uapi/linux/netfilter.h头文件中,定义如下:
enum nf_inet_hooks {
//PREROUTING链,在路由操作前处理的所有钩子函数需要注册到该链上
NF_INET_PRE_ROUTING,
//INPUT链,将要进入本地传输层处理之前的钩子函数需要注册到该链上
NF_INET_LOCAL_IN,
NF_INET_FORWARD, //FORWARD链,路由转发时,内核会执行该链上的函数
NF_INET_LOCAL_OUT, //OUTPUT链,本地发出的数据包将会通过该链
//POSTROUITING链,数据包在进入数据链路层前,会执行该链上的函数
NF_INET_POST_ROUTING,
NF_INET_NUMHOOKS,
......
};
- pf:协议信息。如果是IPv4数据包过滤,该字段值为NFPROTO_IPV4;如果是IPv6数据包过滤,该字段值为NFPROTO_IPV6;ARP数据包过滤,该字段为NFPROTO_ARP;网桥上的数据包过滤,该字段为NFPROTO_BRIDGE。
- in、out:这两个变量是网卡信息,in表示数据包从哪个网卡进入,out表示数据包将要从哪个网卡发出。
- sk:如果该链的处理与套接字关联,该变量表示对应的网络套接字信息。
- net:网络命名空间(network namespace)。命名空间是Linux提供的一种轻量级的虚拟化技术,网络命名空间允许系统建立隔离的网络环境,不同的网络命名空间拥有独立的网络资源及处理流程。可以为不同的网络命名空间分配不同网卡,创建不同的路由表、防火墙规则等。系统在启动时,定义了一个默认的网络命名空间,这个变量为struct net init_net。如果系统启动后,不额外创建网络命名空间,则init_inet就是Linux中唯一的网络命名空间,所有网络数据包的都在init_net命名空间处理。
- okfn:Netfilter的某条链上所有的钩子函数都执行完成且返回数据包被接收,将执行该函数。该函数的第一个参数是网络命名空间;第二个参数是网络套接字信息;第三个参数是对应的网络数据包。其中,第三个参数struct sk_buff结构体在网络数据包处理中尤为重要,它包含了一个网络数据包的所有信息。
(2)将自定义函数注册到INPUT链上
定义好钩子函数后,需要将函数注册到Netfilter的某条链上,数据包经过这条链时,才会被自定义钩子函数处理。要完成钩子函数的注册,需要了解如下的接口:
一、注册钩子函数
int nf_register_net_hooks(struct net *net, const struct nf_hook_ops *reg, unsigned int n)
该函数用于注册钩子函数。通过调用这个函数将自定义的钩子函数注册到Netfilter中,可以一次性注册多个钩子函数。函数的第一个参数net表示该钩子函数应该注册到的网络命名空间;第二个参数reg为注册的操作函数集合,是一个数组,这个接口允许一次注册多个钩子函数;第三个参数n为需要注册的钩子函数的个数,是第二个参数reg数组中元素的个数。其中,第二个参数reg的结构体struct nf_hook_ops定义在Linux内核源码的include/linux/netfilter.h文件中,如下所示。
struct nf_hook_ops {
nf_hookfn *hook; //钩子函数指针
struct net_device *dev; //网卡设备
void *priv; //私有数据,用户可自定义,将被作为第一个参数传入nf_hookfn函数
u_int8_t pf; //协议类型,NFPROTO_IPV4表示IPv4
unsigned int hooknum; //该函数挂载到哪条链上
int priority; //该函数的优先级,数值越小,优先级越高
};
在IPv4中,上述结构体的成员变量hooknum可以为:NF_INET_PRE_ROUTING、NF_INET_FORWARD、NF_INET_LOCAL_IN、NF_INET_LOCAL_OUT、NF_INET_POST_ROUTING,分别对应Netfilter的PREROUTING、FORWARD、INPUT、OUTPUT、POSTROUTING链;变量priority为函数的优先级,数值越小,越早被执行。
Linux操作系统维护了多张钩子函数表,对于每一种协议类型在内存中均存在一张或多张钩子函数表。对于IPv4,Linux系统共维护了五张表,分别对应INPUT、OUTPUT、PREROUTING、POSTROUTING、FORWARD五条链,每条链维护一张表,如下图所示。

在每张表中,越靠前的钩子点优先级越高,在整条链中越先执行,优先级由struct nf_hook_ops结构体的priority成员决定。关于IPv4协议的优先级定义在内核源码的include/uapi/linux/netfilter_ipv4.h头文件中,值越小代表优先级越高,具体定义如下:
enum nf_ip_hook_priorities {
NF_IP_PRI_FIRST = INT_MIN, //该优先级最高,对应的钩子函数最先执行
NF_IP_PRI_RAW_BEFORE_DEFRAG = -450,
NF_IP_PRI_CONNTRACK_DEFRAG = -400,
......
NF_IP_PRI_CONNTRACK_HELPER = 300,
NF_IP_PRI_CONNTRACK_CONFIRM = INT_MAX, //该优先级最低
NF_IP_PRI_LAST = INT_MAX,
};
二、注销钩子函数
void nf_unregister_net_hooks(struct net *net, const struct nf_hook_ops *reg, unsigned int hookcount)
该函数用于注销钩子函数。函数参数与注册函数的参数一致,第一个参数net表示该钩子函数应该注册到的网络命名空间,init_net表示初始网络命名空间;第二个参数reg为钩子函数集合,第三个参数hookcount为需要注销的钩子函数的个数。
2.2 实现Netfilter模块
下面将实现一个最简单的Netfilter模块,该模块的作用是在INPUT连上丢弃所有IP数据包。源文件netfilter_drop_all.c如下:
#include <linux/module.h>
#include <linux/netfilter_ipv4.h>
//将要注册的钩子函数,作用是丢弃所有数据包
static unsigned int drop_all_input(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)
{
//返回NF_DROP丢弃数据包
return NF_DROP;
}
//钩子操作数组,只有一个成员,用于在INPUT链上丢弃数据包
static const struct nf_hook_ops netfilter_mod_ops[] = {
{
.hook = drop_all_input, //将要注册的钩子函数
.pf = NFPROTO_IPV4, //协议为IPv4
.hooknum= NF_INET_LOCAL_IN,//注册在INPUT链上
.priority = NF_IP_PRI_FIRST, //该操作在INPUT链上优先级最高
},
};
//内核模块初始化函数
static int netfilter_drop_all_init(void)
{
//调用nf_register_net_hooks注册钩子函数数组,ARRAY_SIZE用于获取数组的大小
return nf_register_net_hooks(&init_net, netfilter_mod_ops, ARRAY_SIZE(netfilter_mod_ops));
return 0;
}
//内核模块卸载函数
static void netfilter_drop_all_exit(void)
{
//注销netfilter钩子函数数组
nf_unregister_net_hooks(&init_net, netfilter_mod_ops, ARRAY_SIZE(netfilter_mod_ops));
}
module_init(netfilter_drop_all_init);
module_exit(netfilter_drop_all_exit);
上述源码的数组变量netfilter_mod_ops就是需要注册的钩子函数集合,该数组中只有一个数组元素,即注册的钩子函数只有一个。其中协议pf为NFPROTO_IPV4,表示需要过滤IPv4数据包;钩子点hooknum是NF_INET_LOCAL_IN,表示函数需要挂在INPUT链上;优先级priority为NF_IP_PRI_FIRST,通过源码15 5可以看出,该优先级是最高优先级,在数据包进入INPUT链后,会最先执行示例中挂载的钩子函数;钩子函数hook为drop_all_input,在该函数的实现中,仅仅返回了NF_DROP,表示进入INPUT链的所有数据包都将被丢弃。
在内核模块初始化函数中,通过nf_register_net_hooks注册了钩子函数,传入的第一个参数init_net是默认的网络命名空间,只要在系统启动后,不额外创建网络命名空间,init_net就是唯一的网络命名空间。在内核模块卸载函数中,通过nf_unregister_net_hooks注销了钩子函数,模块卸载后,钩子函数不再起作用。
2.3 执行编写的Netfilter模块
对模块进行编译后,先不加载。通过ifconfig命令查看将要加载该模块的终端IP地址,如下所示。

可以看到,终端的IP地址是192.168.126.146。此时在局域网另一台终端上执行命令:ping 192.168.126.146,该命令用于查看与终端192.168.126.146的连通性,执行结果如图下:

上图的执行结果表示另一台终端和192.168.126.146这台终端能够连通,ping命令执行成功。然后在192.168.126.146上加载编译好的内核模块,再次在另一台终端上执行相同的ping命令,发现ping操作失败,如图下所示:

ping命令执行失败的原因是:在执行ping命令时,将会向192.168.126.146这台终端发送IP数据包。数据包到达192.168.126.146这台终端后,会通过数据链路层的处理到达网络层。到达网络层后,会首先执行Netfilter的PREROUTING链上的处理。IP数据包穿过PREROUTING链后进行路由操作,由于数据包的目的IP地址是192.168.126.146,这个IP就是终端自身的IP,此时内核会把数据包交给INPUT链处理。数据包到达INPUT链后,最先执行的函数就是示例程序中的钩子函数drop_all_input,该函数返回NF_DROP,会丢弃所有数据包,因此数据包被丢弃,ping操作失败。卸载内核模块后,ping操作能再次成功。
尝试将drop_all_input函数的返回值改为NF_ACCEPT,数据包将不再被丢弃。如果将返回值改为NF_STOLEN,表示该数据包不再进行后续Linux网络协议栈的处理,数据包被“偷走”。此时应注意返回前需要释放数据包的内存空间。

被折叠的 条评论
为什么被折叠?



