Netfilter内核 API 解析

Netfilter内核 API 解析 Netfilter Hook 管理最基础1. 注册 / 注销钩子#include linux/netfilter.h #include linux/netfilter_ipv4.h // 注册钩子 int nf_register_net_hook(struct net *net, const struct nf_hook_ops *ops); // 注销钩子 void nf_unregister_net_hook(struct net *net, const struct nf_hook_ops *ops);2. 钩子结构体struct nf_hook_ops { nf_hookfn *hook; // 钩子处理函数 struct module *owner; // THIS_MODULE int pf; // 协议 NFPROTO_IPV4 int hooknum; // 钩子点 int priority; // 优先级 };3. 钩子函数原型typedef unsigned int nf_hookfn(void *priv, struct sk_buff *skb, const struct nf_hook_state *state);NFQUEUE 核心 APINFQUEUE 由两部分组成Netfilter 框架抓包、钩子拦截、转交队列子系统nfnetlink_queue 子系统维护队列 用户进程绑定关系封装 Netlink 消息内核→用户态发包接收用户裁决、重新注入协议栈依赖模块nfnetlink底层 Netfilter 专用 Netlink 总线nfnetlink_queueNFQUEUE 队列管理核心关键核心数据结构1.struct nf_queue_handler每个队列号对应一个队列处理器挂在全局队列哈希表记录绑定的用户态 portid队列最大长度、超时、回调2.struct nf_queue_entry每个被放入队列的skb 封装项持有struct sk_buff *skb钩子状态nf_hook_state唯一 packet id所属网络命名空间net超时定时器3. Netlink 侧复用标准struct sock nfnetlink 消息头nlmsghdr→nfgenmsg→nfqnl_msg_packet_hdr→ 原始报文 payload1.nf_queue— 把数据包发送到用户态内核→用户#include net/netfilter/nf_queue.h unsigned int nf_queue(void *priv, struct sk_buff *skb, const struct nf_hook_state *state, unsigned int queuenum);作用将数据包入队内核主动发给用户态。2.nf_reinject— 用户裁决后把数据包放回内核void nf_reinject(struct sk_buff *skb, const struct nf_hook_state *state, unsigned int verdict);3. 队列 entry 管理struct nf_queue_entry *nf_queue_entry_get(u32 id, struct net *net); void nf_queue_entry_free(struct nf_queue_entry *entry);完整内核流转原理步骤 1数据包经过 Netfilter 钩子数据包走到挂载点NF_INET_PRE_ROUTING / LOCAL_IN / FORWARD / LOCAL_OUT / POST_ROUTING你在 hook 里调用return nf_queue(priv, skb, state, queue_num);步骤 2nf_queue()内部做了什么校验队列号合法性查找nf_queue_handler按 queue_num分配nf_queue_entry封装 skb 钩子现场挂入队列等待队列调度返回NF_QUEUE告知 netfilter包已转交队列暂停协议栈流转步骤 3封装 nfnetlink 消息队列子系统分配 Netlink skb填充nlmsghdrnfgenmsgnfqnl_msg_packet_hdrpacket_id、hook、协议族拷贝原始 IP 报文作为 payload根据队列绑定的用户态 portid调用netlink_unicast发给用户进程 这就是内核主动发数据给用户态的底层真相。步骤 4用户态接收、解析、裁决用户通过 nfnetlink socketrecvmsg拿到消息解析 nlmsghdr → nfgenmsg → nfqnl 包头取出 packet_id、原始报文规则判断后构造VERDICT 裁决消息通过同一个 socket 发回内核步骤 5内核接收用户裁决nfnetlink 收到底层消息解析 verdict、packet_id、裁决动作ACCEPT/DROP根据 packet_id 查到nf_queue_entry调用nf_reinject()步骤 6nf_reinject重新注入协议栈nf_reinject(skb, state, verdict);NF_ACCEPT继续后续 netfilter 钩子 协议栈流转NF_DROP直接释放 skb丢弃NF_STOLEN内核接管不继续流转队列绑定原理用户进程怎么和队列挂钩用户态创建NETLINK_NETFILTERsocket执行bind指定自身nl_pid getpid()发送NFQNL_MSG_CONFIG消息绑定队列号内核记录queue_num -- 用户 portid关键点同队列号只能绑定一个用户进程同一进程可以绑定多个不同队列号靠queue_num portid二元组精准路由超时机制每个nf_queue_entry带定时器用户迟迟不返回裁决超时后内核自动DROP防止包滞留内存nf_queue 阻塞当前数据包流转数据包被挂起、暂停内核网络协议栈后续流程等待用户态裁决超时才自动放行 / 丢弃。数据包走到 Netfilter Hook你调用return nf_queue(...)内核不会继续走后续协议栈把 skb 封装成nf_queue_entry放入队列挂起暂停该包的转发 / 上层递送流程等待用户态通过 NFQUEUE 返回裁决ACCEPT/DROP收到裁决后调用nf_reinject才继续流转或丢弃单个数据包是同步阻塞等待裁决的只阻塞当前这一个数据包其他数据包正常进栈、正常过钩子不会阻塞整个网卡、不会阻塞所有流量只是被放入 NFQUEUE 的那个包暂停是异步挂起不是内核线程死等不是忙等、不是阻塞内核进程是skb 暂时脱离协议栈挂在队列中休眠等待不占 CPU。NFQUEUE 内置超时定时器默认超时一般3~10s内核可配置用户态迟迟不发裁决 → 内核自动超时处理超时默认行为自动 DROP 或 ACCEPT内核参数可调作用防止用户态进程崩溃、卡死导致内核数据包永久悬挂、内存泄漏NFQUEUE 阻塞的粒度NFQUEUE 阻塞是per-packet 单包挂起只把当前这一个 skb从协议栈摘出、挂进队列同连接后续数据包照常进栈、照常转发不是阻塞整个连接、不是阻塞网卡队列队列长度限制洪水防护内核有NFQUEUE 队列最大缓存数队列满了之后新来的包直接DROP不再入队避免大量包挂在内核占满内存极端场景大量包同时进 NFQUEUE如果整条流量全都被扔进 NFQUEUE每个包都要等用户态裁决不是时序乱了是整条流时延叠加、吞吐暴跌、排队积压队列满后开始丢包触发 TCP 拥塞控制降速这是流量限速 / 排队问题不是时序错乱问题。会不会影响整机网络不会卡死整机网络只阻塞被匹配进入 NFQUEUE 的流量其他不进队列的流量完全正常转发 / 上网举例你只把TCP 80丢进 NFQUEUE80 流量会被阻塞等裁决SSH、ping、其他端口完全不受影响对 TCP 连接时序的影响1. TCP 自身有超时重传、滑动窗口、乱序缓存TCP 天然设计就是容忍单包延迟中间包慢、前后包先到轻微乱序2. NFQUEUE 慢包带来的现象被阻塞的那个包延迟变大后面的包先到达对端进入 TCP乱序队列out-of-order queue等你 NFQUEUE 裁决放行、慢包终于到达TCP 按原始序列号重新排序向上层交付✅整条连接时序逻辑不变、业务无感知、不会协议错乱3. 不会发生不会 TCP 时序漂移不会序号错乱不会连接断开不会流崩塌4. 会发生单包时延增高轻微乱序重排开销吞吐被拉低尤其小包密集场景对 IP / 二层时序的影响IP 是无连接不可靠本身不保证时序前后包先走、慢包后到是常态NFQUEUE 只是人为增加了某一个包的延时不改变 IP 固有机制对网卡 内核收包队列的影响NFQUEUE 挂起一个包不占网卡 ring buffer网卡继续收后续包、内核继续调度软中断不会卡住网卡收包时序不会导致整网卡吞吐卡死和普通 Netlink 区别普通 Netlink纯消息传递不阻塞数据包NFQUEUE绑定网络报文生命周期天然阻塞数据流怎么规避时延影响用户态epoll 非阻塞极速裁决配置 NFQUEUE超时阈值调低业务分层只拦截控制包不拦截大数据流多队列分散流量避免单队列积压nf_queue 核心调用链路简图skb 到达 Netfilter Hook ↓ return nf_queue() ↓ 分配 nf_queue_entry 封装 skb ↓ 封装 nfnetlink 消息 ↓ netlink_unicast 发给用户 portid ↓ 用户 recv → 规则判断 → 发回 VERDICT ↓ 内核解析裁决 → 查到 entry ↓ nf_reinject 重新注入协议栈 ↓ ACCEPT 继续流转 / DROP 丢弃极简总结NFQUEUE 只阻塞单个数据包不阻塞整条连接。单个包延迟不会打乱 TCP 序列号时序。后续包正常先行到达TCP 乱序缓存自动重排上层无感知。不影响网卡收包时序、不卡死整机网络。副作用时延增大、轻微乱序开销、吞吐下降但协议时序完全正常。数据包skb操作常用 API1. 取 IP 头struct iphdr *ip_hdr(const struct sk_buff *skb);2. 取 TCP 头struct tcphdr *tcp_hdr(const struct sk_buff *skb);3. 取 UDP 头struct udphdr *udp_hdr(const struct sk_buff *skb);访问数据包数据网络包在内核用struct sk_buff *skb管理直接强转访问不安全必须用内核标准工具函数 处理分片 / 非线性 skb。skb_linearizeskb_linearize 把分散在多个内存页的数据包合并成一段连续的内核内存不调用它 → 直接访问skb-data越界 → 内核直接崩溃panic /oops调用它 → 数据变连续 → 安全访问skb-data offsetskb_linearize做了什么它重新拷贝、合并所有分片变成一段连续的内核缓冲区skb-data - 连续大内存块完整的 IP头 TCP头 全部数据合并后skb-data指向连续内存起始skb-len 总长度可以安全用指针偏移访问任何位置不会越界、不会崩溃skb_is_linearskb_is_linear (skb) → 判断这个数据包是否是连续内存 **返回1数据连续线性可以直接安全访问返回0数据不连续非线性、分片存储直接访问会内核崩溃它只是判断不修改任何数据超级轻量非线性 skb数据分散在多个内存页直接访问skb-data offset会越界、panic、宕机所以必须先判断连续 → 直接用不连续 → 必须skb_linearize合并skb_linearize 会修改 skb会重新分配内存会拷贝数据原来的分片会被释放所以它不是只读操作不能在中断上下文随意用可能会睡眠分配内存时HOOK 里可以用因为 HOOK 属于软中断允许睡眠有性能开销合并、拷贝需要时间如果你不需要访问数据就不要调用只在必须读取 payload时调用NFQUEUE 场景必须调用因为你要在内核态解析数据或者要把数据发给用户态解析→ 都要求数据连续skb_pull和skb_pushvoid *skb_pull(struct sk_buff *skb, unsigned int len); void *skb_push(struct sk_buff *skb, unsigned int len);skb_pull(skb, len)向后移数据指针去掉头部剥头skb_push(skb, len)向前移数据指针腾出头部空间加头共同点只改指针不改数据超快形象图解假设数据包初始状态[data] ↑ skb-data (指针在这里)1.skb_pull(skb, 20)向后移 20 字节剥掉 20 字节[ 20字节 | 数据 ] ↑ skb-data 移到这里作用跳过 IP 头、TCP 头直接看 Payload。结果skb-data 20skb-len - 202.skb_push(skb, 20)向前移 20 字节预留 20 字节空间[ 新空间 | 原数据 ] ↑ skb-data 移到这里作用要在前面插入新的协议头比如加个 UDP 头。结果skb-data - 20skb-len 20最经典实战场景场景 1skb_pull剥掉头部读取 Payload前提必须先skb_linearize// 确保数据连续 skb_linearize(skb); struct iphdr *iph ip_hdr(skb); // 1. 剥掉 IP 头 skb_pull(skb, iph-ihl * 4); // 2. 剥掉 TCP 头 struct tcphdr *th (struct tcphdr *)skb-data; skb_pull(skb, th-doff * 4); // ✌️ 现在 skb-data 就是纯应用层数据 printk(Payload: %s, skb-data);场景 2skb_push构造数据包发送时用如果你要修改并发送数据包需要加头// 腾出 TCP 头的空间 struct tcphdr *new_tcp skb_push(skb, sizeof(struct tcphdr)); // 填充 TCP 头部数据 new_tcp-source htons(80);关键注意事项1. 必须配合skb_linearize如果数据包是非线性分散存储直接调用skb_pull会内核崩溃安全模板// 先检查是否连续 if (!skb_is_linear(skb)) { // 不连续就合并失败则丢包 if (skb_linearize(skb)) return NF_DROP; } // 现在才能安全 pull/push skb_pull(skb, len);2. 空间够不够skb_push向前推不能超过skb_headroom头部预留空间否则报错。skb_pull向后拉不能超过skb-len否则返回NULL。3. 与nf_queue的关系如果你只是把包丢给用户态不修改、不看内容不需要pull/push/linearize。如果你在内核态读取 Payload必须走流程linearize→pull→ 读数据关键坑点坑 1skb 是非线性、有分片skb-data只存线性部分后面数据在 frag 页里直接越界宕机。解决方案遍历前先线性化if (skb_linearize(skb) 0) return NF_DROP;坑 2不判断协议直接强转先判iph-protocol再拿 TCP/UDP 头不然非法包直接 oops。坑 3不校验长度就访问必须保证skb-len iphdr_len trans_len防止越界。坑 4网络序大小端端口、IP、序号都是网络字节序要用ntohs/ntohl转主机序。Netfilter 判定verdictNF_ACCEPT // 允许 NF_DROP // 丢弃 NF_STOLEN // 内核接管 NF_QUEUE // 进入队列由nf_queue返回 NF_REPEAT // 重新调用钩子连接跟踪conntrackLinux 连接跟踪conntrack是 Netfilter 内核框架里的状态跟踪子系统核心是用五元组识别流、维护连接状态机、支撑有状态防火墙 / NAT / 负载均衡等功能。连接跟踪Connection Tracking简称 ct内核把每个双向数据流当作一个 “连接”记录其状态、五元组、超时、统计、NAT 映射等后续同流包直接匹配状态不用重复解析规则。五元组Flow Key源 IPsrc_ip目的 IPdst_ip源端口src_port目的端口dst_port四层协议protoTCP/UDP/ICMP双向 Tuple一个连接存两个方向Originalorig发起方向如客户端→服务器Replyreply响应方向服务器→客户端NAT 就是修改 reply 方向的 tuple。核心用途✅ 有状态防火墙iptables -m state --state NEW/ESTABLISHED✅ NATSNAT/DNAT/masquerade依赖 conntrack 保存映射✅ 负载均衡LVS/IPVS 识别同一连接✅ 容器 / 网络虚拟化Docker/OVS 的网络隔离与转发Netfilter 钩子与处理流程conntrack 由nf_conntrack内核模块实现在 Netfilter 关键钩子点介入优先级极高-200。1关键钩子点IPv4PRE_ROUTING入站包第一步nf_conntrack_in查找 / 创建连接LOCAL_OUT本机发出包同上FORWARD转发包匹配已建连接POST_ROUTING出站包最后nf_conntrack_confirm把连接正式加入哈希表2数据包处理四步曲解析 tuple从 IPTCP/UDP 头提取五元组哈希查找查全局 conntrack 哈希表命中→更新状态未命中→新建nf_conn状态机更新按协议TCP/UDP/ICMP更新状态、重置超时Confirm包正常转发 / 路由后把nf_conn存入哈希表中途丢包则放弃保存核心数据结构内核 5.101连接项struct nf_conn一个连接对应一个struct nf_conn { struct nf_conntrack ct_general; // 通用头引用计数、销毁函数 struct nf_conntrack_tuple tuple[IP_CT_DIR_MAX]; // orig reply 五元组 enum ip_conntrack_state state; // 连接状态NEW/ESTABLISHED 等 unsigned long timeout; // 超时时间 struct timer_list timer; // 超时定时器 struct nf_conn_nat *nat; // NAT 扩展映射信息 // 统计、协议私有数据... };2哈希表conntrack_table全局哈希表桶数可配置默认 65536每个桶挂链表存nf_conn查找五元组哈希→桶→遍历链表匹配 tuple#include net/netfilter/nf_conntrack.h // 获取连接跟踪 struct nf_conn *nf_ct_get(const struct sk_buff *skb, enum ip_conntrack_info *ctinfo); // 查找连接 struct nf_conn *nf_ct_find_existing(struct net *net, const struct nf_conntrack_zone *zone, u_int8_t family, const nf_conntrack_tuple *tuple);协议状态机TCP 最复杂UDP 极简1TCP 状态核心 6 种NEW收到 SYN新建连接SYN_SENT发 SYN 后等待 SYNACKSYN_RECV收 SYNACK 后等待 ACKESTABLISHED三次握手完成默认超时 5 天FIN_WAIT收到 FIN半关闭CLOSED连接关闭超时或 RST2UDP 状态无连接仅 2 态UNREPLIED单向流仅一个方向有包默认超时 30 秒ASSURED双向流两个方向都有包默认超时 180 秒3ICMP 状态基于 Echo Request/Reply 匹配超时 30 秒。常见问题与调优1哈希表满nf_conntrack: table full, dropping packet原因并发连接超nf_conntrack_max解决# 临时生效 echo 262144 /proc/sys/net/nf_conntrack_max # 永久生效/etc/sysctl.conf net.nf_conntrack_max262144 sysctl -p2性能损耗conntrack 对每个包做哈希查找与状态更新高并发10 万 PPS会占 CPU调优关闭不必要的协议跟踪如仅跟踪 TCP增大哈希桶数nf_conntrack_buckets对无状态流量如 DNS用iptables -j NOTRACK跳过 conntrack3超时导致连接被误删长连接如 SSH、数据库被超时断开→调大tcp_timeout_established短连接如 HTTP→保持默认或调小netfilter 日志nf_log_packet(net, state-hook, skb, state-in, state-out, loginfo, 自定义日志: );