Netfilter钩子点
通过上文Netfilter介绍的了解到,Netfilter是通过注册钩子函数来将我们的代码加入Netfilter的处理机制中,我们首先需要了解各个钩子点的含义,PRE_ROUTING、LOCAL_IN、FORWARD、LOCAL_OUT和POST_ROUTING是Netfilter钩子点。它们是在Linux内核网络协议栈中定义的五个特定点,用于处理网络数据包的不同阶段。以下是它们的详细介绍:
- PRE_ROUTING:PRE_ROUTING钩子点位于数据包到达Linux内核网络协议栈的最前面,即在数据包进行任何处理之前。在PRE_ROUTING钩子点上注册的回调函数可以访问数据包的目标地址,并可以修改数据包的目标地址。这个钩子点通常用于网络地址转换(NAT)操作。
- LOCAL_IN:LOCAL_IN钩子点位于数据包已经到达Linux内核网络协议栈内部,但尚未被分配给任何应用程序的阶段。在LOCAL_IN钩子点上注册的回调函数可以访问数据包的源地址和目标地址,并可以对数据包进行过滤和修改。这个钩子点通常用于实现网络安全策略和过滤非法数据包。
- FORWARD:FORWARD钩子点用于处理转发的数据包,即那些需要从一个网络接口转发到另一个网络接口的数据包。在FORWARD钩子点上注册的回调函数可以访问数据包的源地址和目标地址,并可以对数据包进行过滤和修改。这个钩子点通常用于实现网络路由策略和流量控制。
- LOCAL_OUT:LOCAL_OUT钩子点用于处理数据包从Linux系统中的应用程序发送到网络的阶段。在LOCAL_OUT钩子点上注册的回调函数可以访问数据包的源地址和目标地址,并可以对数据包进行过滤和修改。这个钩子点通常用于实现网络安全策略和流量控制。
- POST_ROUTING:POST_ROUTING钩子点位于数据包从Linux内核网络协议栈内部发送到网络之前的阶段。在POST_ROUTING钩子点上注册的回调函数可以访问数据包的源地址,并可以修改数据包的源地址。这个钩子点通常用于网络地址转换(NAT)操作。
从上面我们了解到了各个钩子点的含义,比如我们只想接收发到本机的数据包,可以注册LOCAL_IN,如果接收到达本机所有的数据包可以通过注册PRE_ROUTING点。
代码实现
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <linux/ip.h>
static struct nf_hook_ops nfho; // net filter hook option struct
/* function to be called by hook */
unsigned int hook_func(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)
{
struct iphdr *iph; // ip header struct
iph = ip_hdr(skb); // get ip header from skb
if (skb->protocol == htons(ETH_P_IP)) {
// IPv4 packet
/* print ip address and protocol of captured packet */
printk(KERN_INFO "Captured packet: src_addr=%pI4, dst_addr=%pI4, protocol=%d\n",
&iph->saddr, &iph->daddr, iph->protocol);
}
return NF_ACCEPT; // accept the packet
}
/* module init function */
static int __init init_nf_module(void)
{
nfho.hook = hook_func; // hook function
nfho.hooknum = NF_INET_PRE_ROUTING; // hook point
nfho.pf = PF_INET; // protocol family
nfho.priority = NF_IP_PRI_FIRST; // hook priority
nf_register_hook(&nfho); // register hook
printk(KERN_INFO "nf_hook registered\n");
return 0;
}
/* module exit function */
static void __exit exit_nf_module(void)
{
nf_unregister_hook(&nfho); // unregister hook
printk(KERN_INFO "nf_hook unregistered\n");
}
module_init(init_nf_module);
module_exit(exit_nf_module);
MODULE_LICENSE("GPL");
以上代码定义了一个hook_func函数,该函数将被注册到NF_INET_PRE_ROUTING钩子点,当有数据包通过该钩子点时,hook_func函数将被调用,然后抓取数据包的源地址、目的地址和协议,并将这些信息打印出来。同时,在init_nf_module函数中,我们定义了nfho结构体,该结构体包含了钩子函数、钩子点、协议族和钩子优先级等信息,并将其注册到内核的netfilter框架中。最后,在exit_nf_module函数中,我们将钩子函数从netfilter框架中注销
编译
在编译过程中,您需要确保系统已经安装了内核头文件。内核头文件包含了编写内核模块所需的所有头文件和宏定义等内容。
如果您的系统上没有安装内核头文件,您可以使用以下命令安装:
sudo apt-get install linux-headers-$(uname -r)
此命令将安装与当前正在运行的内核版本相对应的内核头文件。
另外,如果您使用的是不同版本的内核,需要使用相应的内核头文件。您可以使用以下命令列出可用的内核头文件:
sudo apt-cache search linux-headers
可以根据需要安装相应的内核头文件。
Makefile
obj-m += nf_hook.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
以上makefile将nf_hook.c编译为一个内核模块,并使用obj-m来定义内核模块的名称。使用make命令可以编译内核模块。
运行
在终端中,使用以下命令将内核模块加载到系统中:
sudo insmod nf_hook.ko
查看系统日志以确保模块已成功加载:
dmesg | tail
在终端中使用ping命令或其他网络工具向目标主机发送数据包,例如:
ping 8.8.8.8
应该能够在系统日志中看到类似以下输出的信息:
[ 184.907485] Captured packet: src_addr=192.168.0.1, dst_addr=8.8.8.8, protocol=1
这表明程序已成功通过netfilter钩子函数抓取了发送到8.8.8.8的ICMP数据包。
在终端中使用以下命令卸载内核模块:
sudo rmmod nf_hook
struct nf_hook_ops
struct nf_hook_ops 结构体中的 nfho 字段是一个 struct nf_hook_ops 类型的指针,它是用来注册 netfilter 钩子函数的。
在使用 netfilter 钩子函数时,我们需要创建一个 struct nf_hook_ops 结构体并将其注册到内核中,以便在网络流量通过内核时触发我们的钩子函数。该结构体中的 nfho 字段是我们需要填充的一个结构体,用于指定我们的钩子函数需要处理的网络流量类型、执行的优先级等信息。
以下是一个简单的 struct nf_hook_ops 结构体的定义:
static struct nf_hook_ops nfho = {
.hook = hook_func, // 指定钩子函数
.hooknum = NF_INET_PRE_ROUTING, // 钩子函数所处的网络流量阶段
.pf = PF_INET, // IP 协议族
.priority = NF_IP_PRI_FIRST // 优先级
};
在上述结构体中,我们填充了以下信息:
- hook 字段:该字段指定了我们需要执行的钩子函数。
- hooknum 字段:该字段指定了钩子函数所处的网络流量阶段。在这个例子中,我们使用了
NF_INET_PRE_ROUTING,该字段表示我们的钩子函数将在 IP 数据包到达主机之前进行处理。 - pf 字段:该字段指定了 IP 协议族。在这个例子中,我们使用了 PF_INET,表示我们的钩子函数将处理 IPv4 数据包。
- priority字段:该字段指定了钩子函数的优先级,以确保在多个钩子函数共存时,执行顺序能够正确确定。在这个例子中,我们使用了NF_IP_PRI_FIRST,表示我们的钩子函数优先级最高。
在定义好 struct nf_hook_ops 结构体之后,我们可以将其注册到内核中,以便触发钩子函数。注册的方法如下:
nf_register_hook(&nfho);
其中,nf_register_hook() 函数用于注册我们的钩子函数,该函数将在我们的钩子函数触发时被调用。
如果我们需要在程序结束时将钩子函数从内核中注销,我们可以使用以下代码:
nf_unregister_hook(&nfho);
skb结构体
在 Linux 内核中,skb(socket buffer)是一个数据结构,用于在网络协议栈中传递数据。它包含了一个数据包的所有信息,如数据、协议头、网络接口信息等等。
在 Linux 内核中,每当一个数据包通过网络接口被接收或者发送时,就会被包装成一个 skb。skb 在整个网络协议栈中传递,直到被交付给应用程序或者发送到网络上。
skb 可以被认为是一个链表,每个 skb 都有一个指向下一个 skb 的指针。当一个 skb 被处理完成后,它会被释放并从链表中移除。
在 Linux 内核中,skb 的结构体定义如下:
struct sk_buff {
struct sk_buff_head __rcu *list;
kmemcheck_bitfield_begin
union {
unsigned char nfct_info[4];
struct {
unsigned int nfct:16;
unsigned int nfctinfo:8;
unsigned int nfnl:8;
};
};
kmemcheck_bitfield_end
unsigned char cb[48];
unsigned int len,
data_len;
__u16 mac_len,
hdr_len;
union {
__be16 protocol;
unsigned long _skb_refdst;
};
unsigned int vlan_tci;
union {
__u16 vlan_proto;
void *sk;
};
__u16 queue_mapping;
unsigned char protocol;
unsigned char priority;
unsigned char local_df:1,
cloned:1,
ip_summed:2,
nohdr:1,
nfct_reasm:1,
nfct_policy:2,
csum_valid:1;
unsigned char pkt_type:3,
fclone:2,
ipvs_property:1,
peeked:1,
nf_trace:1;
union {
__u16 encapsulation;
__u16 encap_hdr_len;
};
/* sk_buff data buffer */
struct skb_shared_info * shinfo;
atomic_t users;
};
其中,skb 的成员包括:
list:一个指向 skb 列表头的指针,用于将 skb 连接成链表。
nfct_info:一个用于记录 skb 状态的字段,用于在网络连接追踪(conntrack)中标记 skb 状态。
cb:skb 内核私有数据区域,用于存储协议层的私有信息。
len:skb 的总长度,包括所有的协议头和数据。
data_len:skb 中数据的长度。
mac_len:skb 中 MAC 地址的长度。
hdr_len:skb 中协议头的长度。
protocol:skb 中的网络协议类型。
vlan_tci:skb 中 VLAN 标签的 TCI 值。
vlan_proto:skb 中 VLAN 标签的协议类型。
queue_mapping:skb 所属的网络队列编号。
protocol:skb 中的协议类型。
priority:skb 的优先级。
local_df:skb 是否启用本地 DF 标志。
cloned:skb 是否是克隆的。
ip_summed:skb 是否需要校验和。
nohdr:skb 中是否包含协议头。
nfct_reasm:skb 是否正在进行连接追踪的数据包重组。
nfct_policy:skb 在连接追踪中的处理策略。
csum_valid:skb 的校验和是否有效。
pkt_type:skb 的包类型。
fclone:skb 的克隆标志。
ipvs_property:skb 是否为 IPVS 负载均衡属性包。
peeked:skb 是否被读取过。
nf_trace:skb 是否需要在网络连接追踪中进行跟踪。
encapsulation:skb 中的封装
encap_hdr_len:skb 中封装协议头的长度。
shinfo:skb 共享信息的指针,包括共享数据缓冲区和共享数据信息。
users:skb 的使用计数器。
在使用 netfilter 进行网络数据包处理时,我们通常需要使用 skb 结构体来访问网络数据包的内容。通过访问 skb 结构体的字段,我们可以获取到数据包的各个部分的内容,从而进行网络数据包的过滤、修改、重定向等操作。
当我们使用 netfilter 进行网络数据包处理时,经常会用到一些与 skb 结构体相关的函数。以下是一些常用的 skb 函数:
skb_copy():用于复制一个 skb 结构体,可以用于重定向数据包等场景。
skb_push() 和 skb_pull():用于在 skb 数据区域的开头和末尾添加或删除数据。
skb_trim():用于调整 skb 的大小。
skb_clone():用于复制一个 skb 结构体的副本。
skb_put():用于向 skb 数据区域中添加数据。
skb_copy_bits():用于从一个 skb 数据区域中复制一段数据。
skb_mac_header()、skb_network_header() 和 skb_transport_header():
用于获取 skb 数据区域中 MAC、网络和传输层头部的指针。
skb_reset_network_header() 和 skb_reset_transport_header():
用于重新设置 skb 数据区域中的网络和传输层头部指针。
这些函数可以帮助我们在处理网络数据包时,方便地访问和修改 skb 结构体中的各个字段,从而实现各种网络数据包的处理功能。
后续讲解我们如何通过自己手动构造skb来实现通信