本文主要翻译自 Using NFQUEUE and libnetfilter_queue,将 C 代码例子换成 Go 代码例子。


介绍

NFQUEUE 是一种 iptables 和 ip6tables 的目标(an iptables and ip6tables target),将网络包处理决定委托给用户态软件。比如,下面的规则将所有接收到的网络包(all packet going to the box)委托给监听中的用户态程序去决策。

1
iptables -A INPUT -j NFQUEUE --queue-num 0

用户态软件必须监听队列 0(connect to queue 0),并从内核获取消息;然后给每个网络包给出判决(verdict)。

内部实现

为了理解 NFQUEUE,最简单的方式是理解 Linux 内核内部架构。当一个网络包触发 NFQUEUE 目标,它将被插入到 --queue-num 指定的队列里。网络包队列是一个链式列表,列表元素是网络包和元数据(Linux 内核 skb):

  • 固定长度的链式列表
  • 使用整数索引网络包
  • 当用户态程序对指定整数索引的网络包做出判决后,该网络包将被释放掉
  • 当队列满了,队列不再接收网络包

用户态程序会有如下影响:

  • 批量读取消息,并批量给出判决。当队列未满时,批量处理不会产生影响。
  • 可以无序地给网络包做出判决。比如,得到 1、2、3、4 网络包,以 4、2、3、1 顺序做出判决。
  • 过慢的判决会导致队列充满。此时,内核会丢掉新来的网络包,而不是插入队列。

内核和用户态之间的通信协议

内核和用户态之间的通信协议使用的是 nfnetlink。这是一种不共享内存的基于消息的通信协议。当一个网络包入队时,内核将包含了网络包数据和相关信息的 nfnetlink 消息发送到某个套接字(socket),用户态程序就能够读取到这条消息。用户态程序进行判决的做法是,将包含了网络包整数索引的 nfnetlink 消息发送到那个套接字。

Go 例子

使用 Go 对接 iptables-nfqueue 的例子

高级特性

多队列

--queue-balance 是 NFQUEUE 选项,由 Florian Westphal 实现,实现了同一条 iptables 规则的网络包负载均衡到多个队列。用法非常简单。比如,负载均衡 INPUT 流量到 0-3 队列的规则如下:

1
iptables -A INPUT -j NFQUEUE --queue-balance 0:3

注意,负载均衡是基于流实现的(made with respect to the flow),一条流的所有网络包会发送到同一个队列。

可用范围:Linux 内核 >= 2.6.31,iptables >= 1.4.5。

--queue-bypass

--queue-bypass 也是由 Florian Westphal 实现的 NFQUEUE 选项。当没有用户态程序监听队列时,不再是丢包,而是放行。

适用范围:Linux 内核 >= 2.6.39,iptables >= 1.4.11。

该特性不适用于内核 3.10 到 3.12 版本。

失败开关

从 Linux 内核 3.6 版本开始,当队列满了,该选项允许放行而不是丢包。

批量判决

从 Linux 内核 3.1 版本开始,能够使用批量判决了。不再是一个网络包做一次判决,能够对 ID 范围的所有网络包只做一次判决(send a verdict to all packets with an id inferior to a given id)。

这能够提高系统处理网络包的性能,但会增大网络包的延迟。所以,用户态程序需要更快地去发出判决消息,以此减小延迟;此时需要注意很少网络包的场景。

杂项

nfnetlink_queue 在 /proc 下的路径:/proc/net/netfilter/nfnetlink_queue

1
2
cat /proc/net/netfilter/nfnetlink_queue 
   40  23948     0 2 65531     0     0      106  1
  • 队列号:由 --queue-number 指定

  • 对端接口 ID:监听队列的 pid

  • 队列数量:当前在队列里等待的网络包数量

  • 复制模式:0 和 1 只提供元数据;2 同时还会提供限定复制范围的部分网络包内容(a part of packet of size copy range)。

  • 复制范围:放进消息里的网络包长度

  • 队列丢包数量:因队列满了而丢包的网络包数量

  • 用户丢包数量:因不能发送到用户态而丢包的网络包数量。如果该数值不为 0,尝试增大 netlink 缓存大小(increase netlink buffer size)。在程序侧,如果发生丢包,可以看到整数索引不连续了(see gap in packet id)。

  • ID 序列:最后一个网络包的整数索引

  • 1

FAQ

多线程

收包/发包(nfnetlink 包)操作需要被锁保护,防止并发写。

收包和发包是完全分开的操作,它们不共享内存。判决只用到包 ID(packet index)。与此同时,上锁能让不同线程对任何包进行判决。

包重排序

NFQUEUE 很容易地为任意入队的包做包重排序。然而,需要注意的是内核是使用链式列表来排队包的;所以,乱序判决会带来一定的损耗。

零拷贝

内核和用户态之间使用 netlink 进行通信,不存在零拷贝这样的好事。Patrick McHardy 已开始 memory mapped implementation of netlink,所以未来可能会有零拷贝。