Netfilter内核防火墙开发实战:从hook注册到生产级策略引擎 📅 发布时间:2026/9/15 4:34:19 👁 浏览次数: 简介本资源是一套基于Linux内核Netfilter框架实现的轻量级网络层防火墙完整项目面向嵌入式开发、网络安全初学者及课程设计/毕设实践者解决IP与端口级数据包过滤的实际工程问题。项目分驱动层与应用层底层驱动filter.ko在Linux 3.13内核中通过nf_hook钩子注册自定义过滤规则上层UI采用Qt5开发支持可视化配置过滤规则。压缩包共19个文件含3个核心C/C源码filter.c、netfilter.cpp等、2个Qt界面文件.ui、2个头文件、Makefile与Kbuild构建脚本、驱动模块编译产物.ko/.o、用户手册.docx及README说明整体927KB结构清晰便于理解内核模块与用户态协同机制。已有203人学习下载提供从驱动编写、编译加载到图形化策略配置的全流程实践材料特别适合掌握Netfilter编程模型与Qt跨层交互开发的进阶学习。1. 为什么还在用 iptables 做策略Netfilter 框架才是 Linux 防火墙的底层真相你可能每天都在敲iptables -A INPUT -p tcp --dport 22 -j ACCEPT却没意识到这条命令背后没有“iptables 服务”也没有独立进程——它只是用户空间的一个配置工具真正干活的是内核里的 Netfilter 框架。Netfilter 不是某个软件而是一套嵌入 Linux 网络协议栈各关键钩子hook的内核基础设施在数据包进入、转发、离开前它能精确拦截、检查、修改甚至丢弃每一个 IP 包。这意味着所有基于 Linux 的防火墙行为——无论是系统默认的 firewalld、docker 的网络隔离还是自研安全网关——最终都必须经由 Netfilter 的五个预设 hook 点NF_INET_PRE_ROUTING、NF_INET_LOCAL_IN、NF_INET_FORWARD、NF_INET_LOCAL_OUT、NF_INET_POST_ROUTING完成策略落地。本文面向有 Linux 网络基础的运维、安全工程师和嵌入式开发者不讲抽象理论只聚焦如何从零构建一个可调试、可扩展、不依赖 userspace 工具链的 Netfilter 模块级防火墙。你会看到如何绕过 iptables 直接注册 hook 函数、如何解析 TCP 头提取端口与标志位、如何按源 IP目标端口做黑白名单决策、以及最关键的——如何避免因 hook 返回值错误导致整机网络中断。这不是教学 demo而是生产环境可复用的轻量级策略引擎底座。2. 在内核模块中注册 Netfilter hook从加载到拦截第一个数据包Netfilter 的能力必须通过内核模块注入才能生效。与用户态工具不同模块直接运行在内核空间对性能敏感且容错极低。因此注册 hook 不是简单调用函数而是一系列严格时序与内存安全操作的组合。2.1 编写最小可运行模块骨架以下代码定义了一个仅注册NF_INET_LOCAL_INhook 的模块用于拦截本机接收的数据包即目标为本机 IP 的入向流量。注意该 hook 位于 IP 层解包后、路由决策前此时 skb 已完成 IP 头解析但尚未进入传输层处理。// nf_firewall.c #include linux/module.h #include linux/kernel.h #include linux/netfilter.h #include linux/netfilter_ipv4.h #include linux/ip.h #include linux/tcp.h #include linux/skbuff.h static struct nf_hook_ops nfho; unsigned int hook_func(void *priv, struct sk_buff *skb, const struct nf_hook_state *state) { struct iphdr *iph; if (!skb) return NF_ACCEPT; iph ip_hdr(skb); if (iph iph-protocol IPPROTO_TCP) { struct tcphdr *tcph tcp_hdr(skb); printk(KERN_INFO NF_HOOK: TCP packet to port %d\n, ntohs(tcph-dest)); } return NF_ACCEPT; // 允许通过 } static int __init nf_firewall_init(void) { printk(KERN_INFO NF Firewall module loaded\n); nfho.hook hook_func; nfho.hooknum NF_INET_LOCAL_IN; nfho.pf PF_INET; nfho.priority NF_IP_PRI_FIRST; // 最高优先级最早被调用 nfho.owner THIS_MODULE; if (nf_register_net_hook(init_net, nfho) 0) { printk(KERN_ERR Failed to register hook\n); return -1; } return 0; } static void __exit nf_firewall_exit(void) { nf_unregister_net_hook(init_net, nfho); printk(KERN_INFO NF Firewall module unloaded\n); } module_init(nf_firewall_init); module_exit(nf_firewall_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(Engineer); MODULE_DESCRIPTION(Minimal Netfilter firewall hook);提示NF_IP_PRI_FIRST表示该 hook 将在所有其他NF_INET_LOCAL_IN注册者之前执行。若需在 iptables 规则之后再干预例如做日志审计应改用NF_IP_PRI_CONNTRACK 1或更低优先级值。优先级数值越小执行越早。2.2 编译与加载Makefile 与内核版本强绑定Netfilter API 在不同内核版本间存在细微差异如struct nf_hook_ops字段名变更、nf_register_net_hook替代旧版nf_register_hook因此 Makefile 必须显式指定当前运行内核头文件路径# Makefile obj-m nf_firewall.o KDIR : /lib/modules/$(shell uname -r)/build all: make -C $(KDIR) M$(PWD) modules clean: make -C $(KDIR) M$(PWD) clean install: sudo insmod nf_firewall.ko uninstall: sudo rmmod nf_firewall执行make sudo insmod nf_firewall.ko后可通过dmesg | tail -10查看是否打印NF Firewall module loaded。此时模块已驻留内核但尚未产生实际拦截效果——因为hook_func当前始终返回NF_ACCEPT。下一步需加入真实策略逻辑。2.3 验证 hook 是否生效用 tcpdump dmesg 双向确认仅靠dmesg日志不足以证明 hook 正常工作因为日志可能被淹没或延迟。可靠验证方式是构造可控流量并观察响应在终端 A 运行sudo tcpdump -i lo tcp port 8080 -nn -c 2监听本地回环接口 TCP 8080在终端 B 执行curl -v http://127.0.0.1:8080触发一个 TCP SYN 包立即在终端 C 执行dmesg | grep NF_HOOK若看到类似NF_HOOK: TCP packet to port 8080的输出且tcpdump捕获到 SYN 包则证明 hook 已成功挂载并收到数据包。注意此阶段不要返回NF_DROP否则 curl 将超时失败影响后续调试。2.4 关键陷阱skb 内存生命周期与协议头指针有效性skb结构体在 Netfilter hook 中是只读引用其内部数据缓冲区skb-data指向 IP 头起始位置。但ip_hdr(skb)和tcp_hdr(skb)宏并非简单偏移计算它们依赖skb-network_header和skb-transport_header字段。这些字段在某些场景下如经过nf_ct_attach连接跟踪、或被xt_TPROXY修改可能未正确设置直接调用会导致内核 oops。安全做法是显式校验if (!pskb_may_pull(skb, sizeof(struct iphdr))) return NF_ACCEPT; // 数据包太短无法解析 IP 头 iph ip_hdr(skb); if (iph-ihl 5 || iph-version ! 4) return NF_ACCEPT; if (iph-protocol IPPROTO_TCP) { if (!pskb_may_pull(skb, iph-ihl * 4 sizeof(struct tcphdr))) return NF_ACCEPT; tcph (struct tcphdr *)((u8 *)iph iph-ihl * 4); }pskb_may_pull是 Netfilter 提供的安全函数确保 skb 数据区至少包含指定字节数避免越界访问。这是模块稳定性的第一道防线不可省略。3. 实现黑白名单策略基于源 IP 与目标端口的实时决策引擎真正的防火墙不能只打印日志必须根据规则做出放行NF_ACCEPT、丢弃NF_DROP或排队NF_QUEUE动作。本节构建一个内存驻留的黑白名单机制支持动态添加/删除规则且不依赖用户态守护进程。3.1 设计轻量级哈希表存储规则为避免线性遍历影响性能采用内核提供的rhashtableradix hash table。相比传统hash_table它支持无锁并发插入/查找适合多 CPU 场景。定义规则结构如下struct fw_rule { __be32 src_ip; // 网络字节序 __be16 dst_port; // 网络字节序 u8 action; // FW_ACTION_ALLOW or FW_ACTION_DENY struct rhash_head node; }; enum { FW_ACTION_ALLOW 0, FW_ACTION_DENY 1, }; static struct rhashtable rules_ht; static const struct rhashtable_params rules_rht_params { .head_offset offsetof(struct fw_rule, node), .key_offset offsetof(struct fw_rule, src_ip), .key_len sizeof(__be32), .max_size 1024, .min_size 16, };注意此处key_offset指向src_ip意味着哈希仅基于源 IP 计算。若需联合src_ip dst_port查询需定义复合 key 结构并重写rhashtable_params中的obj_hashfn和obj_cmpfn回调函数。本例为简化先实现单维度匹配。3.2 在 hook 函数中执行策略匹配将规则匹配逻辑嵌入hook_func并在匹配成功时返回对应动作unsigned int hook_func(void *priv, struct sk_buff *skb, const struct nf_hook_state *state) { struct iphdr *iph; struct tcphdr *tcph; struct fw_rule *rule; __be32 src_ip; __be16 dst_port; if (!skb) return NF_ACCEPT; iph ip_hdr(skb); if (!iph || iph-protocol ! IPPROTO_TCP) return NF_ACCEPT; if (!pskb_may_pull(skb, iph-ihl * 4 sizeof(struct tcphdr))) return NF_ACCEPT; tcph (struct tcphdr *)((u8 *)iph iph-ihl * 4); src_ip iph-saddr; dst_port tcph-dest; // 查找匹配规则 rule rhashtable_lookup_fast(rules_ht, src_ip, rules_rht_params); if (rule rule-action FW_ACTION_DENY) { printk(KERN_INFO BLOCKED: %pI4:%d - %pI4:%d\n, src_ip, ntohs(src_ip), iph-daddr, ntohs(dst_port)); return NF_DROP; } return NF_ACCEPT; }3.3 提供用户态接口通过 netlink 动态管理规则内核模块不能直接读取文件或监听 socket必须借助 netlink 与用户态通信。我们定义一个简单的 netlink 协议族NETLINK_FIREWALL支持ADD_RULE、DEL_RULE、LIST_RULES三条命令#define NETLINK_FIREWALL 31 struct fw_nl_msg { __u8 cmd; // NL_CMD_ADD / NL_CMD_DEL __be32 src_ip; __be16 dst_port; __u8 action; }; static struct sock *fw_nl_sock; static void nl_recv_msg(struct sk_buff *skb) { struct nlmsghdr *nlh; struct fw_nl_msg *msg; struct fw_rule *rule; nlh nlmsg_hdr(skb); if (nlh-nlmsg_len NLMSG_LENGTH(sizeof(*msg))) return; msg nlmsg_data(nlh); switch (msg-cmd) { case NL_CMD_ADD: rule kzalloc(sizeof(*rule), GFP_KERNEL); if (!rule) return; rule-src_ip msg-src_ip; rule-dst_port msg-dst_port; rule-action msg-action; rhashtable_insert_fast(rules_ht, rule-node, rules_rht_params); break; case NL_CMD_DEL: rule rhashtable_lookup_fast(rules_ht, msg-src_ip, rules_rht_params); if (rule) { rhashtable_remove_fast(rules_ht, rule-node, rules_rht_params); kfree(rule); } break; } } static int __init nf_firewall_init(void) { // ... 其他初始化 ... // 初始化 netlink socket struct netlink_kernel_cfg cfg { .input nl_recv_msg, }; fw_nl_sock netlink_kernel_create(init_net, NETLINK_FIREWALL, cfg); if (!fw_nl_sock) { printk(KERN_ERR Failed to create netlink socket\n); return -1; } // 初始化哈希表 if (rhashtable_init(rules_ht, rules_rht_params) 0) { netlink_kernel_release(fw_nl_sock); return -1; } return 0; }3.4 用户态控制程序用 C 实现规则增删查编写一个简易用户态工具fwctl通过 netlink 发送指令// fwctl.c #include stdio.h #include stdlib.h #include string.h #include unistd.h #include sys/socket.h #include linux/netlink.h #include linux/socket.h #define NETLINK_FIREWALL 31 #define MAX_PAYLOAD 1024 struct fw_nl_msg { __u8 cmd; __be32 src_ip; __be16 dst_port; __u8 action; }; int main(int argc, char *argv[]) { struct sockaddr_nl src_addr, dest_addr; struct nlmsghdr *nlh NULL; struct iovec iov; int sock_fd, ret; struct msghdr msg; sock_fd socket(AF_NETLINK, SOCK_RAW, NETLINK_FIREWALL); if (sock_fd 0) { perror(socket); return 1; } memset(src_addr, 0, sizeof(src_addr)); src_addr.nl_family AF_NETLINK; src_addr.nl_pid getpid(); bind(sock_fd, (struct sockaddr*)src_addr, sizeof(src_addr)); nlh (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD)); memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD)); nlh-nlmsg_len NLMSG_SPACE(sizeof(struct fw_nl_msg)); nlh-nlmsg_pid getpid(); nlh-nlmsg_flags 0; struct fw_nl_msg *msg_data (struct fw_nl_msg *)NLMSG_DATA(nlh); msg_data-cmd atoi(argv[1]); // 1: add, 2: del msg_data-src_ip inet_addr(argv[2]); msg_data-dst_port htons(atoi(argv[3])); msg_data-action atoi(argv[4]); iov.iov_base (void *)nlh; iov.iov_len nlh-nlmsg_len; msg.msg_name (void *)dest_addr; msg.msg_namelen sizeof(dest_addr); msg.msg_iov iov; msg.msg_iovlen 1; ret sendmsg(sock_fd, msg, 0); if (ret 0) perror(sendmsg); close(sock_fd); free(nlh); return 0; }编译后使用sudo ./fwctl 1 192.168.1.100 22 1→ 添加拒绝 192.168.1.100 访问本机 22 端口的规则sudo ./fwctl 2 192.168.1.100 22 1→ 删除该规则至此一个具备动态策略能力的 Netfilter 防火墙核心已成型无需重启模块即可更新规则。4. 高级策略TCP 标志位检测与连接状态感知仅靠 IP端口匹配属于“无状态过滤”无法应对 SYN Flood、FIN 扫描等攻击。Netfilter 支持深度解析 TCP 头部标志位SYN、ACK、FIN、RST并结合连接跟踪conntrack实现有状态检测。本节展示如何识别异常 TCP 握手行为并主动阻断。4.1 解析 TCP 标志位并识别 SYN Flood 特征正常 TCP 握手为Client 发送SYN→ Server 回SYN-ACK→ Client 发ACK。若某 IP 在短时间内发送大量SYN但无后续ACK即为典型 SYN Flood。我们在 hook 中统计每个源 IP 的未完成连接数struct syn_flood_tracker { __be32 src_ip; u32 syn_count; // 10 秒窗口内 SYN 数 unsigned long last_update; struct rhash_head node; }; static struct rhashtable syn_ht; static const struct rhashtable_params syn_rht_params { .head_offset offsetof(struct syn_flood_tracker, node), .key_offset offsetof(struct syn_flood_tracker, src_ip), .key_len sizeof(__be32), }; // 在 hook_func 中添加 if (tcph-syn !tcph-ack) { struct syn_flood_tracker *tracker; unsigned long now jiffies; tracker rhashtable_lookup_fast(syn_ht, src_ip, syn_rht_params); if (!tracker) { tracker kzalloc(sizeof(*tracker), GFP_ATOMIC); if (!tracker) return NF_ACCEPT; tracker-src_ip src_ip; rhashtable_insert_fast(syn_ht, tracker-node, syn_rht_params); } if (time_after(now, tracker-last_update 10 * HZ)) { tracker-syn_count 0; tracker-last_update now; } tracker-syn_count; if (tracker-syn_count 50) { // 阈值可调 printk(KERN_WARNING SYN Flood detected from %pI4, blocked\n, src_ip); return NF_DROP; } }注意GFP_ATOMIC表示在中断上下文安全分配内存因 Netfilter hook 可能在软中断中执行禁止调用可能睡眠的GFP_KERNEL。4.2 与内核 conntrack 模块联动识别非法 FIN/RSTNetfilter 自带连接跟踪子系统nf_conntrack可获取数据包所属连接的状态IPS_ESTABLISHED、IPS_SEEN_REPLY等。要判断一个FIN包是否合法需确认其对应连接已建立且对方已回复过数据#include net/netfilter/nf_conntrack.h #include net/netfilter/nf_conntrack_core.h struct nf_conn *ct; enum ip_conntrack_info ctinfo; ct nf_ct_get(skb, ctinfo); if (ct ct-status IPS_SEEN_REPLY tcph-fin) { // 合法 FIN允许 } else if (tcph-fin !(ct ct-status IPS_SEEN_REPLY)) { // 未见过回复就发 FIN疑似扫描 return NF_DROP; }启用 conntrack 需在模块初始化时加载依赖// 在 init 函数中 if (!nf_ct_l3proto_pernet_register(init_net, nf_ip_l3proto)) return -1;4.3 防御端口扫描检测连续端口探测行为攻击者常使用nmap -sS扫描目标开放端口表现为同一源 IP 在极短时间内向多个端口发送 SYN。我们维护一个滑动窗口记录最近 5 秒内该 IP 访问过的端口集合#define MAX_PORTS_PER_IP 64 struct port_scan_tracker { __be32 src_ip; __be16 ports[MAX_PORTS_PER_IP]; u32 count; unsigned long window_start; struct rhash_head node; }; // 在 hook 中 if (tcph-syn !tcph-ack) { struct port_scan_tracker *tracker; int i; tracker rhashtable_lookup_fast(scan_ht, src_ip, scan_rht_params); if (!tracker) { tracker kzalloc(sizeof(*tracker), GFP_ATOMIC); if (!tracker) return NF_ACCEPT; tracker-src_ip src_ip; tracker-window_start jiffies; rhashtable_insert_fast(scan_ht, tracker-node, scan_rht_params); } if (time_after(jiffies, tracker-window_start 5*HZ)) { tracker-count 0; tracker-window_start jiffies; } // 检查是否已记录该端口 for (i 0; i tracker-count; i) { if (tracker-ports[i] tcph-dest) break; } if (i tracker-count tracker-count MAX_PORTS_PER_IP) { tracker-ports[tracker-count] tcph-dest; if (tracker-count 20) { // 5 秒内扫超 20 个端口 printk(KERN_WARNING Port scan from %pI4\n, src_ip); return NF_DROP; } } }5. 生产就绪技巧日志分级、性能压测与 panic 防护模块上线前必须解决三个现实问题日志爆炸导致磁盘占满、高并发下性能骤降、以及因 bug 引发内核 panic。本节提供可直接集成的工程化方案。5.1 限频日志用 ratelimit 控制 printk 输出printk在高流量场景下会严重拖慢系统。内核提供printk_ratelimit()和printk_ratelimited()宏基于时间窗口限制输出频率if (printk_ratelimit()) { printk(KERN_INFO DROPPED %pI4:%d - %pI4:%d\n, src_ip, ntohs(src_ip), iph-daddr, ntohs(dst_port)); }更精细的控制可使用DEFINE_RATELIMIT_STATE定义独立限速器static DEFINE_RATELIMIT_STATE(rs, 5 * HZ, 10); // 5秒内最多10条 if (__ratelimit(rs)) { printk(KERN_INFO High-frequency drop from %pI4\n, src_ip); }5.2 性能压测用 pktgen 构造 10Gbps 流量验证吞吐在物理机或高性能 VM 上使用内核自带的pktgen工具生成线速流量测试模块在极限压力下的表现# 加载 pktgen modprobe pktgen # 配置发包网卡假设为 eth0 echo add_device eth0 /proc/net/pktgen/kpktgend_0 echo device eth0 /proc/net/pktgen/eth0 echo flag IPSRC_RND /proc/net/pktgen/eth0 # 随机源IP echo count 0 /proc/net/pktgen/eth0 # 无限发包 echo pkt_size 64 /proc/net/pktgen/eth0 echo dst 192.168.1.100 /proc/net/pktgen/eth0 echo udp_dst_min 1000 /proc/net/pktgen/eth0 echo udp_dst_max 65535 /proc/net/pktgen/eth0 echo start /proc/net/pktgen/eth0同时监控 CPU 使用率watch -n1 cat /proc/interrupts | grep eth0。若 softirq CPU 占用持续高于 70%说明 hook 处理逻辑过重需优化如减少内存分配、合并条件判断。5.3 Panic 防护用 try_module_get/try_module_put 避免卸载时崩溃当模块正在处理数据包时用户执行rmmod会导致nf_firewall_exit被调用若此时hook_func仍在运行rhashtable可能已被销毁引发 oops。标准防护模式是在 hook 开始处增加模块引用计数unsigned int hook_func(...) { if (!try_module_get(THIS_MODULE)) { return NF_ACCEPT; // 获取失败跳过处理 } // 主逻辑... module_put(THIS_MODULE); // 必须配对调用 return ret; }try_module_get在模块状态为LIVE时返回 true并增加引用计数module_put减少计数。当计数归零时rmmod才能真正卸载模块确保无残留执行流。5.4 规则持久化将内存规则导出为 userspace 配置文件为防止机器重启后规则丢失可在模块退出前将当前规则快照写入/var/lib/nf-firewall/rules.confstatic void save_rules_to_file(void) { struct file *fp; char buf[256]; struct fw_rule *rule; struct rhashtable_iter iter; fp filp_open(/var/lib/nf-firewall/rules.conf, O_WRONLY|O_CREAT|O_TRUNC, 0600); if (IS_ERR(fp)) return; rhashtable_walk_enter(rules_ht, iter); while ((rule rhashtable_walk_next(iter)) ! NULL) { if (IS_ERR(rule)) continue; snprintf(buf, sizeof(buf), %pI4 %d %d\n, rule-src_ip, ntohs(rule-dst_port), rule-action); kernel_write(fp, buf, strlen(buf), fp-f_pos); } rhashtable_walk_exit(iter); filp_close(fp, NULL); }配合 systemd service在开机时自动加载该文件中的规则实现配置闭环。最后检查dmesg中是否有BUG:或WARNING:开头的行确认无内存泄漏或锁竞争。至此一个基于 Netfilter 框架、具备黑白名单、状态检测、抗压能力和生产防护的防火墙核心已完整实现。本文还有配套的精品资源点击获取