news 2026/5/13 0:06:41

Netfilter内核 API 解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 核心 API

NFQUEUE 由两部分组成:

  1. Netfilter 框架:抓包、钩子拦截、转交队列子系统
  2. nfnetlink_queue 子系统
    • 维护队列 + 用户进程绑定关系
    • 封装 Netlink 消息
    • 内核→用户态发包
    • 接收用户裁决、重新注入协议栈

依赖模块:

  • nfnetlink:底层 Netfilter 专用 Netlink 总线
  • nfnetlink_queue:NFQUEUE 队列管理核心

关键核心数据结构

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 消息头:nlmsghdrnfgenmsgnfqnl_msg_packet_hdr→ 原始报文 payload

1.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);

步骤 2:nf_queue()内部做了什么

  1. 校验队列号合法性
  2. 查找nf_queue_handler(按 queue_num)
  3. 分配nf_queue_entry,封装 skb + 钩子现场
  4. 挂入队列等待队列调度
  5. 返回NF_QUEUE告知 netfilter:包已转交队列,暂停协议栈流转

步骤 3:封装 nfnetlink 消息

队列子系统:

  1. 分配 Netlink skb
  2. 填充:
    • nlmsghdr
    • nfgenmsg
    • nfqnl_msg_packet_hdr(packet_id、hook、协议族)
    • 拷贝原始 IP 报文作为 payload
  3. 根据队列绑定的用户态 portid
  4. 调用netlink_unicast发给用户进程

👉 这就是内核主动发数据给用户态的底层真相。

步骤 4:用户态接收、解析、裁决

用户通过 nfnetlink socketrecvmsg拿到消息:

  • 解析 nlmsghdr → nfgenmsg → nfqnl 包头
  • 取出 packet_id、原始报文
  • 规则判断后,构造VERDICT 裁决消息
  • 通过同一个 socket 发回内核

步骤 5:内核接收用户裁决

nfnetlink 收到底层消息:

  1. 解析 verdict、packet_id、裁决动作(ACCEPT/DROP)
  2. 根据 packet_id 查到nf_queue_entry
  3. 调用nf_reinject()

步骤 6:nf_reinject重新注入协议栈

nf_reinject(skb, state, verdict);
  • NF_ACCEPT:继续后续 netfilter 钩子 & 协议栈流转
  • NF_DROP:直接释放 skb,丢弃
  • NF_STOLEN:内核接管,不继续流转

队列绑定原理(用户进程怎么和队列挂钩)

  1. 用户态创建NETLINK_NETFILTERsocket
  2. 执行bind,指定自身nl_pid = getpid()
  3. 发送NFQNL_MSG_CONFIG消息绑定队列号
  4. 内核记录:queue_num <--> 用户 portid

关键点

  • 同队列号只能绑定一个用户进程
  • 同一进程可以绑定多个不同队列号
  • queue_num + portid二元组精准路由

超时机制

每个nf_queue_entry带定时器:

  • 用户迟迟不返回裁决
  • 超时后内核自动DROP防止包滞留内存

nf_queue 阻塞当前数据包流转

数据包被挂起、暂停内核网络协议栈后续流程,等待用户态裁决,超时才自动放行 / 丢弃。

  1. 数据包走到 Netfilter Hook,你调用return nf_queue(...)
  2. 内核不会继续走后续协议栈
  3. 把 skb 封装成nf_queue_entry放入队列挂起
  4. 暂停该包的转发 / 上层递送流程
  5. 等待用户态通过 NFQUEUE 返回裁决(ACCEPT/DROP)
  6. 收到裁决后调用nf_reinject,才继续流转或丢弃

👉单个数据包是同步阻塞等待裁决的

只阻塞当前这一个数据包:

  • 其他数据包正常进栈、正常过钩子
  • 不会阻塞整个网卡、不会阻塞所有流量
  • 只是被放入 NFQUEUE 的那个包暂停

是异步挂起,不是内核线程死等:

不是忙等、不是阻塞内核进程;是skb 暂时脱离协议栈,挂在队列中休眠等待,不占 CPU。

NFQUEUE 内置超时定时器

  • 默认超时一般3~10s(内核可配置)
  • 用户态迟迟不发裁决 → 内核自动超时处理
  • 超时默认行为:自动 DROP 或 ACCEPT(内核参数可调)

作用:防止用户态进程崩溃、卡死,导致内核数据包永久悬挂、内存泄漏

NFQUEUE 阻塞的粒度

NFQUEUE 阻塞是per-packet 单包挂起

  • 只把当前这一个 skb从协议栈摘出、挂进队列
  • 同连接后续数据包照常进栈、照常转发
  • 不是阻塞整个连接、不是阻塞网卡队列

队列长度限制(洪水防护)

内核有NFQUEUE 队列最大缓存数

  • 队列满了之后,新来的包直接DROP,不再入队
  • 避免大量包挂在内核占满内存

极端场景:大量包同时进 NFQUEUE

如果整条流量全都被扔进 NFQUEUE,每个包都要等用户态裁决:

  • 不是时序乱了
  • 整条流时延叠加、吞吐暴跌、排队积压
  • 队列满后开始丢包,触发 TCP 拥塞控制降速

这是流量限速 / 排队问题,不是时序错乱问题。

会不会影响整机网络?

  • 不会卡死整机网络
  • 只阻塞被匹配进入 NFQUEUE 的流量
  • 其他不进队列的流量完全正常转发 / 上网

举例:你只把TCP 80丢进 NFQUEUE

  • 80 流量会被阻塞等裁决
  • SSH、ping、其他端口完全不受影响

对 TCP 连接时序的影响

1. TCP 自身有超时重传、滑动窗口、乱序缓存

TCP 天然设计就是容忍:

  • 单包延迟
  • 中间包慢、前后包先到
  • 轻微乱序

2. NFQUEUE 慢包带来的现象

  1. 被阻塞的那个包延迟变大
  2. 后面的包先到达对端,进入 TCP乱序队列(out-of-order queue)
  3. 等你 NFQUEUE 裁决放行、慢包终于到达
  4. TCP 按原始序列号重新排序,向上层交付

整条连接时序逻辑不变、业务无感知、不会协议错乱

3. 不会发生:

  • 不会 TCP 时序漂移
  • 不会序号错乱
  • 不会连接断开
  • 不会流崩塌

4. 会发生:

  • 单包时延增高
  • 轻微乱序重排开销
  • 吞吐被拉低(尤其小包密集场景)

对 IP / 二层时序的影响

  • IP 是无连接不可靠,本身不保证时序
  • 前后包先走、慢包后到是常态
  • NFQUEUE 只是人为增加了某一个包的延时,不改变 IP 固有机制

对网卡 & 内核收包队列的影响

  • NFQUEUE 挂起一个包不占网卡 ring buffer
  • 网卡继续收后续包、内核继续调度软中断
  • 不会卡住网卡收包时序
  • 不会导致整网卡吞吐卡死

和普通 Netlink 区别

  • 普通 Netlink:纯消息传递,不阻塞数据包
  • NFQUEUE:绑定网络报文生命周期,天然阻塞数据流

怎么规避时延影响

  1. 用户态epoll 非阻塞极速裁决
  2. 配置 NFQUEUE超时阈值调低
  3. 业务分层:只拦截控制包,不拦截大数据流
  4. 多队列分散流量,避免单队列积压

nf_queue 核心调用链路简图

skb 到达 Netfilter Hook ↓ return nf_queue() ↓ 分配 nf_queue_entry 封装 skb ↓ 封装 nfnetlink 消息 ↓ netlink_unicast 发给用户 portid ↓ 用户 recv → 规则判断 → 发回 VERDICT ↓ 内核解析裁决 → 查到 entry ↓ nf_reinject 重新注入协议栈 ↓ ACCEPT 继续流转 / DROP 丢弃

极简总结

  1. NFQUEUE 只阻塞单个数据包,不阻塞整条连接。
  2. 单个包延迟,不会打乱 TCP 序列号时序
  3. 后续包正常先行到达,TCP 乱序缓存自动重排,上层无感知。
  4. 不影响网卡收包时序、不卡死整机网络。
  5. 副作用:时延增大、轻微乱序开销、吞吐下降,但协议时序完全正常。

数据包(skb)操作常用 API

1. 取 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_linearize

skb_linearize = 把分散在多个内存页的数据包,合并成一段连续的内核内存

  • 不调用它 → 直接访问skb->data越界 → 内核直接崩溃(panic /oops)
  • 调用它 → 数据变连续 → 安全访问skb->data + offset
skb_linearize做了什么?

重新拷贝、合并所有分片,变成一段连续的内核缓冲区

skb->data -> 连续大内存块(完整的 IP头 + TCP头 + 全部数据)

合并后:

  • skb->data指向连续内存起始
  • skb->len= 总长度
  • 可以安全用指针偏移访问任何位置
  • 不会越界、不会崩溃
skb_is_linear

skb_is_linear (skb) → 判断这个数据包是否是连续内存 **

  • 返回1:数据连续(线性),可以直接安全访问
  • 返回0:数据不连续(非线性、分片存储),直接访问会内核崩溃

只是判断,不修改任何数据,超级轻量!

  • 非线性 skb数据分散在多个内存页
  • 直接访问skb->data + offset越界、panic、宕机
  • 所以必须先判断:
    • 连续 → 直接用
    • 不连续 → 必须skb_linearize合并
skb_linearize 会修改 skb!
  • 重新分配内存
  • 拷贝数据
  • 原来的分片会被释放
  • 所以它不是只读操作
不能在中断上下文随意用
  • 可能会睡眠(分配内存时)
  • HOOK 里可以用,因为 HOOK 属于软中断,允许睡眠
有性能开销
  • 合并、拷贝需要时间
  • 如果你不需要访问数据,就不要调用
  • 只在必须读取 payload时调用
NFQUEUE 场景必须调用

因为:

  • 你要在内核态解析数据
  • 或者要把数据发给用户态解析→ 都要求数据连续

skb_pullskb_push

void *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 += 20
    • skb->len -= 20
2.skb_push(skb, 20)向前移 20 字节(预留 20 字节空间)
[ 新空间 | 原数据 ] ↑ skb->data 移到这里!
  • 作用:要在前面插入新的协议头(比如加个 UDP 头)。
  • 结果
    • skb->data -= 20
    • skb->len += 20
最经典实战场景
场景 1:skb_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);
场景 2:skb_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必须走流程:linearizepull→ 读数据

关键坑点

坑 1:skb 是非线性、有分片

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 判定(verdict)

NF_ACCEPT // 允许 NF_DROP // 丢弃 NF_STOLEN // 内核接管 NF_QUEUE // 进入队列(由nf_queue返回) NF_REPEAT // 重新调用钩子

连接跟踪(conntrack)

Linux 连接跟踪(conntrack)是 Netfilter 内核框架里的状态跟踪子系统,核心是用五元组识别流、维护连接状态机、支撑有状态防火墙 / NAT / 负载均衡等功能。

连接跟踪(Connection Tracking,简称 ct):内核把每个双向数据流当作一个 “连接”,记录其状态、五元组、超时、统计、NAT 映射等,后续同流包直接匹配状态,不用重复解析规则。

五元组(Flow Key)

  • 源 IP(src_ip)
  • 目的 IP(dst_ip)
  • 源端口(src_port)
  • 目的端口(dst_port)
  • 四层协议(proto:TCP/UDP/ICMP)

双向 Tuple

一个连接存两个方向:

  • Original(orig):发起方向(如客户端→服务器)
  • Reply(reply):响应方向(服务器→客户端)NAT 就是修改 reply 方向的 tuple。

核心用途

  • ✅ 有状态防火墙:iptables -m state --state NEW/ESTABLISHED
  • ✅ NAT:SNAT/DNAT/masquerade(依赖 conntrack 保存映射)
  • ✅ 负载均衡:LVS/IPVS 识别同一连接
  • ✅ 容器 / 网络虚拟化:Docker/OVS 的网络隔离与转发

Netfilter 钩子与处理流程

conntrack 由nf_conntrack内核模块实现,在 Netfilter 关键钩子点介入,优先级极高(-200)。

1)关键钩子点(IPv4)

  • PRE_ROUTING:入站包第一步,nf_conntrack_in查找 / 创建连接
  • LOCAL_OUT:本机发出包,同上
  • FORWARD:转发包,匹配已建连接
  • POST_ROUTING:出站包最后,nf_conntrack_confirm把连接正式加入哈希表

2)数据包处理四步曲

  1. 解析 tuple:从 IP+TCP/UDP 头提取五元组
  2. 哈希查找:查全局 conntrack 哈希表,命中→更新状态;未命中→新建nf_conn
  3. 状态机更新:按协议(TCP/UDP/ICMP)更新状态、重置超时
  4. Confirm:包正常转发 / 路由后,把nf_conn存入哈希表;中途丢包则放弃保存

核心数据结构(内核 5.10+)

1)连接项: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 极简)

1)TCP 状态(核心 6 种)
  • NEW:收到 SYN,新建连接
  • SYN_SENT:发 SYN 后等待 SYN+ACK
  • SYN_RECV:收 SYN+ACK 后等待 ACK
  • ESTABLISHED:三次握手完成(默认超时 5 天)
  • FIN_WAIT:收到 FIN,半关闭
  • CLOSED:连接关闭(超时或 RST)
2)UDP 状态(无连接,仅 2 态)
  • UNREPLIED:单向流(仅一个方向有包),默认超时 30 秒
  • ASSURED:双向流(两个方向都有包),默认超时 180 秒
3)ICMP 状态

基于 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_max=262144 sysctl -p

2)性能损耗

  • conntrack 对每个包做哈希查找与状态更新,高并发(10 万 + PPS)会占 CPU
  • 调优:
    • 关闭不必要的协议跟踪(如仅跟踪 TCP)
    • 增大哈希桶数(nf_conntrack_buckets
    • 对无状态流量(如 DNS)用iptables -j NOTRACK跳过 conntrack

3)超时导致连接被误删

  • 长连接(如 SSH、数据库)被超时断开→调大tcp_timeout_established
  • 短连接(如 HTTP)→保持默认或调小

netfilter 日志

nf_log_packet(net, state->hook, skb, state->in, state->out, loginfo, "自定义日志: ");
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/13 0:01:43

知识管理工具横评:技术人用Notion还是Obsidian?

——软件测试从业者的深度选型指南一、测试工程师的知识管理困境在软件测试的日常中&#xff0c;我们面对的是海量的用例、不断迭代的需求、复杂的缺陷关联以及跨团队的沟通信息。传统文件夹加Excel的模式早已不堪重负&#xff1a;用例版本混乱导致回归遗漏&#xff0c;经验教训…

作者头像 李华
网站建设 2026/5/12 23:56:18

A2A 开放协议草案 v0.6

A2A 开放协议草案 v0.6 Agent-to-Agent Open Protocol Draft v0.6 版本: 0.6.0 | 2026-05-10 起草方: 若辰 &#x1f31f;、若兰 &#x1f338;、承宏 &#x1f916;、阿轩 &#x1f527; 审阅方: 明德 &#x1f38b; 维护者: 碳硅契社区 (CSB Community) 状态: ✍️ v0.6 草案…

作者头像 李华