Linux内核Netfilter使用实战案例分析
一、NF_DROP演示
该代码是演示案例,丢弃ICMP包并打印,可以用ping命令查看。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <linux/ip.h>
#include <linux/icmp.h>
// 定义钩子回调函数
static unsigned int drop_icmp_hook(void *priv, struct sk_buff *skb, const struct nf_hook_state *state) {
struct iphdr *iph;
struct icmphdr *icmph;
if (!skb)
return NF_ACCEPT;
iph = ip_hdr(skb);
if (iph->protocol == IPPROTO_ICMP) {
icmph = icmp_hdr(skb);
// 丢弃 ICMP 数据包
printk(KERN_INFO "Dropping ICMP packet\n");
return NF_DROP;
}
return NF_ACCEPT;
}
static struct nf_hook_ops drop_icmp_ops = {
//该字段是一个函数指针,指向实际的钩子回调函数。
.hook = drop_icmp_hook,
//指定协议族,即钩子函数要处理的数据包所属的协议类型。NFPROTO_IPV4 表示处理 IPv4 协议的数据包。如果要处理 IPv6 协议的数据包,则应使用 NFPROTO_IPV6。
.pf = NFPROTO_IPV4,
//指定钩子函数要挂载的 Netfilter 挂接点。
.hooknum = NF_INET_PRE_ROUTING,
//指定钩子函数的优先级。在同一个挂接点上可能会挂载多个钩子函数,内核会根据优先级来决定这些钩子函数的执行顺序。
.priority = NF_IP_PRI_FIRST,
};
static int __init drop_icmp_init(void) {
int ret;
ret = nf_register_net_hook(&init_net, &drop_icmp_ops);
if (ret) {
printk(KERN_ERR "Failed to register netfilter hook\n");
return ret;
}
printk(KERN_INFO "Netfilter hook registered successfully\n");
return 0;
}
static void __exit drop_icmp_exit(void) {
nf_unregister_net_hook(&init_net, &drop_icmp_ops);
printk(KERN_INFO "Netfilter hook unregistered\n");
}
module_init(drop_icmp_init);
module_exit(drop_icmp_exit);
MODULE_LICENSE("GPL");
Makefile
obj-m:=NF_HOOK.o
CURRENT_PAHT:=$(shell pwd)
LINUX_KERNEL:=$(shell uname -r)
LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)
all:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PAHT) modules
clean:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PAHT) clean
编译make
当加载模块之前可以正常ping 8.8.8.8
插入模块之后 无法ping通
卸载模块
二、NF_STOLEN演示
实现了在 NF_INET_PRE_ROUTING
挂接点捕获数据包并进行复制分析,同时阻止原数据包继续传输的功能。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <linux/skbuff.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/udp.h>
#include <linux/icmp.h>
// 模拟数据包分析函数
static void monitor_packet(struct sk_buff *skb) {
struct iphdr *iph;
struct tcphdr *tcph;
struct udphdr *udph;
struct icmphdr *icmph;
if (!skb)
return;
iph = ip_hdr(skb);
if (!iph)
return;
printk(KERN_INFO "Captured packet: Src IP %pI4, Dst IP %pI4, Protocol %u\n",
&iph->saddr, &iph->daddr, iph->protocol);
switch (iph->protocol) {
case IPPROTO_TCP:
tcph = tcp_hdr(skb);
if (tcph)
printk(KERN_INFO " TCP: Src Port %u, Dst Port %u\n",
ntohs(tcph->source), ntohs(tcph->dest));
break;
case IPPROTO_UDP:
udph = udp_hdr(skb);
if (udph)
printk(KERN_INFO " UDP: Src Port %u, Dst Port %u\n",
ntohs(udph->source), ntohs(udph->dest));
break;
case IPPROTO_ICMP:
icmph = icmp_hdr(skb);
if (icmph)
printk(KERN_INFO " ICMP: Type %u, Code %u\n",
icmph->type, icmph->code);
break;
default:
break;
}
}
// 钩子回调函数
static unsigned int steal_pkt_for_monitor(void *priv, struct sk_buff *skb, const struct nf_hook_state *state) {
struct sk_buff *new_skb;
new_skb = skb_clone(skb, GFP_ATOMIC); // 克隆数据包
if (new_skb) {
// 对 new_skb 进行分析(如解析协议内容)
monitor_packet(new_skb);
kfree_skb(new_skb); // 释放克隆的数据包
return NF_STOLEN; // 原数据包不再继续传输
}
return NF_ACCEPT;
}
static struct nf_hook_ops drop_icmp_ops = {
.hook = steal_pkt_for_monitor,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_PRE_ROUTING,
.priority = NF_IP_PRI_FIRST,
};
static int __init monitor_init(void) {
int ret;
ret = nf_register_net_hook(&init_net, &drop_icmp_ops);
if (ret) {
printk(KERN_ERR "Failed to register netfilter hook\n");
return ret;
}
printk(KERN_INFO "Netfilter hook registered successfully\n");
return 0;
}
static void __exit monitor_exit(void) {
nf_unregister_net_hook(&init_net, &drop_icmp_ops);
printk(KERN_INFO "Netfilter hook unregistered\n");
}
module_init(monitor_init);
module_exit(monitor_exit);
MODULE_LICENSE("GPL");
Makefile
obj-m:=NF_STOLEN.o
CURRENT_PAHT:=$(shell pwd)
LINUX_KERNEL:=$(shell uname -r)
LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)
all:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PAHT) modules
clean:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PAHT) clean
编译
插入内核
运行
发现ping已经被拦截
反而被内核自定义模块截获,打印信息:
三、NF_QUEUE演示
将数据包送入用户空间队列,由用户空间程序(如 iptables
配合 libnetfilter_queue
库)进一步处理。
NF_QUEUE.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
// 钩子回调函数,将数据包入队
static unsigned int queue_pkt_hook(void *priv, struct sk_buff *skb, const struct nf_hook_state *state) {
return NF_QUEUE; // 数据包入队,交用户空间处理
}
static struct nf_hook_ops queue_pkt_ops = {
.hook = queue_pkt_hook,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_PRE_ROUTING,
.priority = NF_IP_PRI_FIRST,
};
static int __init nf_queue_init(void) {
int ret;
ret = nf_register_net_hook(&init_net, &queue_pkt_ops);
if (ret) {
printk(KERN_ERR "Failed to register netfilter hook\n");
return ret;
}
printk(KERN_INFO "Netfilter hook registered successfully\n");
return 0;
}
static void __exit nf_queue_exit(void) {
nf_unregister_net_hook(&init_net, &queue_pkt_ops);
printk(KERN_INFO "Netfilter hook unregistered\n");
}
module_init(nf_queue_init);
module_exit(nf_queue_exit);
MODULE_LICENSE("GPL");
nf_queue_user.c
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <arpa/inet.h> //包含 IP 地址转换函数(如 inet_ntoa)
#include <netinet/ip.h> //定义 IP 头部结构体 iphdr
// 手动定义 NF_ACCEPT 为 1,避免因包含内核头文件 linux/netfilter.h 引发的宏冲突
#define NF_ACCEPT 1
//libnetfilter_queue/libnetfilter_queue.h:引入 Netfilter 队列操作的头文件,用于与内核空间的队列交互。
#include <libnetfilter_queue/libnetfilter_queue.h>
// 处理数据包的回调函数
/*
qh:队列句柄。队列句柄是一个指向 struct nfq_q_handle 的指针,它代表了用户空间程序与内核中某个 Netfilter 队列(如队列 0)的连接。每个队列句柄唯一对应内核中的一个队列,通过它可以对该队列进行专属操作(如设置数据包裁决、获取数据包等)
msg:Netfilter 消息头。
nfa:数据包元数据。
data:用户自定义数据(未使用)。
*/
static int process_packet(struct nfq_q_handle *qh, struct nfgenmsg *msg, struct nfq_data *nfa, void *data) {
struct nfqnl_msg_packet_hdr *ph; //指向数据包头部信息的指针
struct iphdr *iph; //指向 IP 头部的指针
u_int32_t id; //数据包 ID
unsigned char *buf; //存储数据包有效负载
int ret; //函数返回值或数据长度
// 从 nfa 中提取数据包头部信息
ph = nfq_get_msg_packet_hdr(nfa);
if (!ph) {
fprintf(stderr, "无法获取数据包头部\n");
return -1;
}
//将数据包 ID 从网络字节序(大端)转换为主机字节序(小端)
id = ntohl(ph->packet_id);
// 打印数据包信息
printf("收到数据包 ID: %u\n", id);
// 解析 IP 头部
//nfq_get_payload(nfa, &buf):获取数据包的有效负载(即 IP 数据包内容),并将长度存入 ret
ret = nfq_get_payload(nfa, &buf);
if (ret >= (int)sizeof(struct iphdr)) {
iph = (struct iphdr *)buf;
printf(" Src IP: %s\n", inet_ntoa(*(struct in_addr *)&iph->saddr));
printf(" Dst IP: %s\n", inet_ntoa(*(struct in_addr *)&iph->daddr));
}
// 放行数据包
/*
nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL):
对数据包做出裁决:NF_ACCEPT 表示放行数据包。
参数依次为队列句柄、数据包 ID、裁决动作、数据长度、额外数据(NULL)。
函数返回 0 表示处理成功。
*/
nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL);
return 0;
}
int main() {
struct nfq_handle *h;
struct nfq_q_handle *qh;
int fd;
int rv;
//buf:存储接收到的数据包数据,__attribute__ ((aligned)) 确保内存对齐
char buf[4096] __attribute__ ((aligned));
// 初始化 Netfilter 队列
h = nfq_open();
if (!h) {
fprintf(stderr, "无法打开 Netfilter 队列\n");
return -1;
}
// buf:存储接收到的数据包数据,__attribute__ ((aligned)) 确保内存对齐
qh = nfq_create_queue(h, 0, &process_packet, NULL);
if (!qh) {
fprintf(stderr, "无法创建队列\n");
nfq_close(h);
return -1;
}
/*
nfq_set_mode(qh, NFQNL_COPY_PACKET, 0xffff):
设置队列模式为 NFQNL_COPY_PACKET,表示将完整的数据包拷贝到用户空间。
0xffff 表示拷贝数据包的最大长度(65535 字节)
*/
if (nfq_set_mode(qh, NFQNL_COPY_PACKET, 0xffff) < 0) {
fprintf(stderr, "无法设置队列模式\n");
return -1;
}
// 处理数据包,获取 Netfilter 队列的文件描述符 fd,用于后续的 recv 操作
fd = nfq_fd(h);
while ((rv = recv(fd, buf, sizeof(buf), rv >= 0))) {
//将接收到的数据传递给 Netfilter 库处理,触发回调函数 process_packet
nfq_handle_packet(h, buf, rv);
}
// 清理
nfq_destroy_queue(qh);
nfq_close(h);
return 0;
}
Makefile
obj-m:= NF_QUEUE.o
CURRENT_PAHT:=$(shell pwd)
LINUX_KERNEL:=$(shell uname -r)
LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)
all:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PAHT) modules
clean:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PAHT) clean
编译
安装库文件
sudo apt update
sudo apt install libnetfilter-queue-dev
开始编译内核模块
编译用户代码
gcc nf_queue_user.c -o nf_queue_user -lnetfilter_queue
插入模块并运行