核心内容摘要
3步搞定USB安全移除:设备管理工具的终极解决方案
在Linux系统中防火墙是网络安全的核心组件而内核级防火墙相比用户态防火墙如iptables用户态工具能直接在网络数据包流经内核协议栈时进行处理避免了用户态-内核态的频繁切换具备更低的延迟和更高的吞吐量。
本文将从核心概念、设计思路、
实现原理等角度拆解一款支持IP单IPCIDR网段、端口单端口端口范围黑白名单管控的低级Linux防火墙理解内核态网络管控的核心逻辑。
先搞懂这些核心概念到底是什么在看具体实现前先把基础概念掰扯清楚避免被专业术语绕晕
核心框架NetfilterNetfilter是Linux内核内置的网络数据包处理框架相当于在内核的网络数据通路上预埋了多个“钩子点”Hook Point。
我们可以把自己的过滤逻辑挂到这些钩子上数据包经过时就会触发我们的代码——本文的防火墙就是把过滤逻辑挂在了NF_INET_PRE_ROUTING钩子点数据包进入协议栈、路由决策前优先级略高于连接跟踪CONNTRACK保证先做黑白名单过滤再处理连接跟踪。
关键数据结构为什么选这些Radix树基数树用来存单IP的黑白名单。
Radix树是内核里专门做“键值对”快速查找的结构单IP作为键32位无符号整数IP的黑白名单标记作为值查一个IP是否在名单里的效率接近O(
比链表遍历快得多。
哈希链表RCU用来存CIDR网段如
192.
168.
0/24。
先通过哈希函数把网段的网络地址映射到哈希桶再用链表存同一个桶里的CIDR规则RCU读-拷贝更新是内核的并发安全机制保证多CPU核同时读规则时不阻塞写规则时不影响读。
位图Bitmap链表用来存端口规则。
端口号范围是
用位图的每2位分别标记一个端口是否在白名单/黑名单比如第port1位标记白名单port1 1位标记黑名单查端口是否命中规则只需一个位运算速度极快链表则用来记录端口范围如
方便规则的添加/删除。
Per-CPU缓存内核每个CPU核都有独立的缓存存最近匹配过的IP/CIDR规则避免同一个IP每次都去查Radix树/哈希链表进一步降低开销。
内核态-用户态交互ProcFSProcFS是Linux内核提供的伪文件系统/proc目录可以通过创建自定义的文件节点让用户态程序比如echo、cat直接读写内核数据。
本文的防火墙就是通过ProcFS创建了一系列文件节点用户只需往/proc下的特定文件写规则如echo
192.
168.
100 /proc/xxx/ip_whitelist/add就能动态添加/删除黑白名单无需重启内核模块。
防火墙的核心设计思路整体设计遵循“高性能、低开销、可动态配置”三个原则核心架构分为三层读写ProcFS文件解析规则/同步数据提供匹配接口Netfilter钩子ACCEPT/DROP用户态ProcFS交互层规则存储层数据包过滤层网络数据包内核协议栈后续处理If you need the complete source code, please add the WeChat number (c
核心决策逻辑数据包进来后防火墙按这个顺序判断只要匹配到一条规则就立刻决策不做多余检查先查是否命中CIDR黑名单 → 命中就丢弃DROP再查是否命中单IP黑名单 → 命中就丢弃接着查是否命中CIDR白名单 → 命中就放行ACCEPT再查是否命中单IP白名单 → 命中就放行如果是TCP/UDP包查目标端口命中端口白名单 → 放行命中端口黑名单 → 丢弃以上都没命中 → 默认丢弃可根据需求调整。
补充先查黑名单再查白名单是为了保证“黑名单优先级更高”——比如一个IP既在白名单又在黑名单最终会按黑名单处理。
核心
实现原理拆解
数据包拦截Netfilter钩子注册想要处理数据包第一步是把我们的过滤函数挂到Netfilter的钩子上// 定义钩子操作结构体staticconststructnf_hook_opsfw_ops{.hookfw_filter,// 数据包过滤的核心函数.pfNFPROTO_IPV4,// 只处理IPv4数据包.hooknumNF_INET_PRE_ROUTING,// 钩子点路由前.priorityNF_IP_PRI_CONNTRACK1,// 优先级比连接跟踪高一点};// 初始化时注册钩子voidfw_net_init(void){nf_register_net_hook(init_net,fw_ops);}这段代码的作用就是告诉内核“凡是IPv4数据包经过PRE_ROUTING阶段时先调用我的fw_filter函数处理”。
核心过滤函数fw_filter这是数据包处理的核心逻辑我们用大白话拆解关键步骤staticunsignedintfw_filter(void*priv,structsk_buff*skb,conststructnf_hook_state*state){structiphdr*ip_headerip_hdr(skb);// 提取IP头u32 src_ipntohl(ip_header-saddr);// 把网络序的源IP转成主机序intret;// 第一步跳过已建立连接的数据包连接跟踪标记过的提升性能structnf_conn*ctnf_ct_get(skb,ctinfo);if(ct)returnNF_ACCEPT;// 第二步检查CIDR黑名单 → IP黑名单 → CIDR白名单 → IP白名单retip_in_cidr_blacklist(src_ip);if(ret)returnNF_DROP;retip_in_blacklist(src_ip);if(ret)returnNF_DROP;retip_in_cidr_whitelist(src_ip);if(ret)returnNF_ACCEPT;retip_in_whitelist(src_ip);if(ret)returnNF_ACCEPT;// 第三步处理TCP/UDP端口规则if(ip_header-protocolIPPROTO_TCP){structtcphdr*tcp_headertcp_hdr(skb);u16 dst_portntohs(tcp_header-dest);// 提取目标端口if(port_in_whitelist(dst_port))returnNF_ACCEPT;if(port_in_blacklist(dst_port))returnNF_DROP;}elseif(ip_header-protocolIPPROTO_UDP){// UDP端口处理逻辑和TCP一致}// 都没匹配到默认丢弃returnNF_DROP;}核心逻辑
总结先跳过已连接的包避免重复过滤再按“黑名单优先”的顺序查IP/CIDR规则最后查端口规则匹配到就立刻返回放行/丢弃。
规则存储不同场景选不同结构1单IP规则Radix树Radix树的优势是“键值对快速查找”适合单IP这种精准匹配的场景// 定义Radix树根节点structradix_tree_rootip_tree;// 初始化Radix树voidfw_ip_init(void){INIT_RADIX_TREE(ip_tree,GFP_KERNEL);}// 查找IP是否在白名单intip_in_whitelist(u32 ip){ip_desc*descradix_tree_lookup(ip_tree,ip);// 核心查找操作if(desc(desc-flagsIP_WHITELIST_MASK)){this_cpu_write(ipcache,desc);// 写入Per-CPU缓存return1;}return0;}往Radix树加IP规则时先查IP是否已存在存在则更新标记不存在则新建节点插入保证规则不重复。
2CIDR规则哈希链表RCUCIDR是网段匹配比如
192.
168.
0/24先把网段的网络地址哈希到桶再遍历桶里的链表找匹配的网段// 哈希桶数组大小是2^16#definebucket_num(
structhlist_head*cidr_hash;// 初始化哈希桶voidfw_cidr_init(void){cidr_hashkmalloc(bucket_num*sizeof(*cidr_hash),GFP_KERNEL);for(inti0;ibucket_num;i)INIT_HLIST_HEAD(cidr_hash[i]);}// CIDR匹配核心先哈希再遍历链表intip_in_cidr_blacklist(u32 ip){inti;u16 hash;cidr_desc*desc;// 遍历已配置的CIDR掩码从大到小保证精准匹配优先for(i0;i32;i){if(cidr_mask_array[i]
continue;// 把IP按掩码截断得到网段地址u32 cidr_ipip~((1cidr_mask_array[i])-
;hashhashfn(cidr_ip);// 哈希计算rcu_read_lock();// RCU读锁保证并发安全// 遍历哈希桶里的链表hlist_for_each_entry_rcu(desc,cidr_hash[hash],node){if(desc-ipcidr_ip(desc-flagsCIDR_BLACKLIST_MASK)){rcu_read_unlock();return1;}}rcu_read_unlock();}return0;}这里有个小技巧CIDR掩码按从大到小遍历比如先/24再/16保证小网段更精准优先匹配。
3端口规则位图链表端口规则分“单端口”和“端口范围”位图负责快速查询链表负责存储范围规则// 位图2*65536位 16KB占用内存极小staticlongunsignedint*port_bitmap;// 初始化位图voidfw_port_init(void){port_bitmapbitmap_zalloc(117,GFP_KERNEL);// 2^17位 16KB}// 快速判断端口是否在白名单位运算O(
intport_in_whitelist(u16 port){returntest_bit((port
,port_bitmap);}// 添加端口范围规则遍历范围设置位图intinsert_port(void*p){port_desc*descp;intstartdesc-start;intenddesc-end?:start;// 单端口则startendif(desc-flagsPORT_WHITELIST_MASK){for(intistart;iend;i){set_bit((i
,port_bitmap);// 设置白名单位}}// 同时把范围规则存到链表方便后续删除// ... 链表插入逻辑 ...return1;}位图的优势是“极致快”查一个端口只需一次位运算比任何链表/树都快链表则是为了记录“范围”避免删除时要遍历所有端口。
用户态交互ProcFS接口为了让用户能动态配置规则防火墙在/proc下创建了一套文件节点比如/proc/xxx/ip/whitelist/add往这里写IP添加IP白名单/proc/xxx/cidr/blacklist/delete往这里写CIDR删除CIDR黑名单/proc/xxx/port/whitelist/show读这个文件查看端口白名单。
核心实现是注册文件操作函数read/write用户写数据时内核解析数据比如把“
192.
168.
0/24”拆成IP和掩码再调用对应的添加/删除函数用户读数据时内核把规则拼接成字符串返回给用户态。
这里要注意并发安全用mutex锁保护规则的添加/删除避免多进程同时写规则导致数据错乱。
核心知识点
总结这款内核级防火墙的实现核心是把“Linux内核网络编程”的关键知识点落地
总结几个核心要点
性能优化的核心思路数据结构适配场景精准匹配单IP用Radix树网段匹配CIDR用哈希链表端口匹配用位图——不同场景选最合适的结构避免“一刀切”减少冗余操作Per-CPU缓存缓存最近匹配的规则跳过已连接的数据包减少重复计算并发安全且高效读规则用RCU无锁读写规则用Mutex低冲突锁兼顾并发安全和性能。
Netfilter使用的关键钩子点选择PRE_ROUTING适合“路由前过滤”能在数据包进入本地进程/转发前就处理优先级设置略高于连接跟踪CONNTRACK避免对已建立的连接重复过滤提升性能决策返回值NF_ACCEPT放行、NF_DROP丢弃是Netfilter的核心返回值决定数据包的命运。
内核态编程的
注意事项内存管理内核态内存不能随意分配要用kmalloc/kfree且要指定分配标志如GFP_KERNEL并发安全多CPU核、多进程操作同一数据时必须用RCU/Mutex等机制保护避免数据竞争用户态-内核态交互用copy_from_user/copy_to_user传输数据不能直接访问用户态内存避免内核崩溃。
应用场景与扩展思路这款内核级防火墙适合轻量级、高性能、低延迟的网络管控场景比如嵌入式设备路由器、网关的网络访问控制服务器的精准IP/端口黑白名单管控高并发场景下的快速数据包过滤。
扩展方向也很明确支持IPv6只需把IP的存储从32位改成128位适配Radix树和哈希函数增强日志记录过滤的数据包信息源IP、端口、时间输出到内核日志或用户态动态开关添加全局开关通过ProcFS控制防火墙的启用/禁用更细粒度的规则支持按协议ICMP/ICMPv
按网卡、按时间段过滤。
总结Linux内核级防火墙的核心本质是“利用内核原生框架Netfilter 适配场景的高效数据结构 安全的并发控制”实现对网络数据包的快速管控。
相比用户态防火墙它少了用户态-内核态的切换开销能在高并发场景下保持极低的延迟而合理选择数据结构Radix树、哈希、位图则是保证过滤性能的关键。
Welcome to follow WeChat official account【程序猿编码】