使用NFQUEUE和libnetfilter_queue

翻译来自这里

介绍

NFQUEUE是一个iptables和ip6tables目标,它将数据包的决定委托给用户空间软件。 例如,以下规则将要求所有去往包的数据包都要听取用户安全计划的决定:

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

在用户空间中,一个软件必须使用libnetfilter_queue连接到队列0(默认的)并从内核获取消息。 然后它必须对包进行判决。

内在工作

要理解NFQUEUE,最简单的方法就是了解Linux内核的架构。 当一个数据包到达一个NFQUEUE目标时,它就会进入与--queue-num选项给定的编号对应的队列。 数据包队列被实现为链接列表,元素是数据包和元数据(Linux内核skb):

  • 这是一个固定长度的队列,实现为数据包的链接列表。
  • 存储由整数索引的数据包
  • 当用户空间向相应的索引整数发出判决时,数据包被释放
  • 当队列满时,没有数据包可以入队

这在用户空间方面有一些含义:

  • 用户空间可以读取多个数据包并等待判决。 如果队列未满,则不会有这种行为的影响。
  • 数据包可以在没有命令的情况下被判决。 用户空间可以读取数据包1,2,3,4并以4,2,3,1的顺序依次判断。
  • 太慢的判决将导致一个完整的队列。 然后内核将丢弃传入的数据包,而不是将它们排入队列。

关于内核和用户空间之间的协议有几句话

内核和用户空间之间使用的协议是nfnetlink。 这是一个基于消息的协议,不涉及任何共享内存。 当一个数据包进入队列时,内核发送一个包含数据包数据和相关信息的nfnetlink格式的消息到一个套接字,用户空间读取这个消息。 为了做出判定,用户空间格式化一个包含索引号的nfnetlink消息并发送给通信套接字。

在C中使用libnetfilter_queue

libnetfiler_queue的主要信息来源是Doxygen生成的文档。

库的使用有三个步骤:

  • 软件库连接到给定队列并设置一些选项的库设置。
  • 消息接收阶段调用每个数据包接收一个回调。
  • 在关闭时候nfq_close被调用。
    如果您想查看产品代码,可以查看一下libnetfilter_queue的多线程实现suricata中的source-nfq.c。

示例软件架构

最简单的架构是由一个线程读取数据包并发布判决。 下面的代码不完整,但显示了实现的逻辑。

/* Definition of callback function */
static int cb(struct nfq_q_handle *qh, struct nfgenmsg *nfmsg,
              struct nfq_data *nfa, void *data)
{
    int verdict;
    u_int32_t id = treat_pkt(nfa, &verdict); /* Treat packet */
    return nfq_set_verdict(qh, id, verdict, 0, NULL); /* Verdict packet */
}
 
/* Set callback function */
qh = nfq_create_queue(h,  0, &cb, NULL);
for (;;) {
    if ((rv = recv(fd, buf, sizeof(buf), 0)) >= 0) {
        nfq_handle_packet(h, buf, rv); /* send packet to callback */
        continue;
    }
}

读线程和判决线程也是可能的:

PacketPool *ppool;
 
/* Definition of callback function */
static int cb(struct nfq_q_handle *qh, struct nfgenmsg *nfmsg,
              struct nfq_data *nfa, void *data)
{
    /* Simply copy packet date and send them to a packet pool */
    return push_packet_to_pool(ppool, nfa);
}
 
int main() {
    /* Set callback function */
    qh = nfq_create_queue(h,  0, &cb, NULL);
    /* create reading thread */
    pthread_create(read_thread_id, NULL, read_thread, qh);
    /* create verdict thread */
    pthread_create(write_thread_id, NULL, verdict_thread, qh);
    /* ... */
}
 
static void *read_thread(void *fd)
{
    for (;;) {
        if ((rv = recv(fd, buf, sizeof(buf), 0)) >= 0) {
            nfq_handle_packet(h, buf, rv); /* send packet to callback */
            continue;
        }
    }
}
 
static void *verdict_thread(void *fd)
{
    for (;;) {
        Packet p = fetch_packet_from_pool(ppool);
        u_int32_t id = treat_pkt(nfa, &verdict); /* Treat packet */
        nfq_set_verdict(qh, id, verdict, 0, NULL); /* Verdict packet */
    }
}

其他语言

Pierre Chifflier(又名pollux)开发了libnetfilter_queue的绑定,可以用于大多数高级语言(python,perl,ruby,...):nfqueue-bindings。

高级功能

多队列

--queue-balance是Florian Westphal添加的NFQUEUE选项,可以将通过相同iptables规则排队的平衡分组加载到多个队列中。 用法相当简单。 例如,要将INPUT流量负载平衡到队列0到3,可以使用以下规则。

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

有一点需要提及的是,负载平衡是针对流而言的,并且流的所有分组都被发送到相同的队列。
这个扩展是从Linux内核2.6.31和iptables v1.4.5开始的。

queue-bypass

--queue-bypass在其他NFQUEUE选项上设置了排队旁路。 当没有用户空间软件连接到队列时,它改变了iptables规则的行为。 如果没有软件正在监听队列,则数据包将被授权,而不是丢弃数据包。

从Linux内核2.6.39和iptables v1.4.11开始扩展。

这个特性从内核3.10到3.12被破坏:当使用最近的iptables时,传递选项--queue-bypass对这些内核没有影响。

fail-open

这个选项在Linux 3.6以后是可用的,并且允许接受数据包,而不是在队列满时丢弃数据包。 一个示例用法可以在suricata中找到。

批量判决

从Linux 3.1开始,可以使用批量判定。 不要为一个数据包发送一个判决,而是可以向一个id低于给定id的数据包发送判决。 为此,必须使用nfq_set_verdict_batch或nfq_set_verdict_batch2函数。

该系统具有性能优势,因为消息的限制增加了分组速率。 但是它可以引起延迟,因为数据包一次被判决。 因此,用户空间软件有责任找到自适应技术,通过更快地发布判决来限制延迟,特别是在数据包较少的情况下。

杂项

/proc中的nfnetlink_queue条目
nfnetlink_queue在/ proc:/proc/net/netfilter/ nfnetlink_queue中有一个专用的入口

cat /proc/net/netfilter/nfnetlink_queue 
   40  23948     0 2 65531     0     0      106  1

695/5000
内容如下:

  • 队列号
  • peer portid:很有可能是软件侦听队列的进程ID
  • 队列总数:当前在队列中等待的数据包数量
  • 复制模式:0和1只有消息只提供元数据。 如果2个消息提供一个大小复制范围的包的一部分。
  • 复制范围:要放入消息的分组数据的长度
  • 队列丢失:由于队列已满而丢弃的数据包数量
  • 用户丢弃:由于无法将netlink消息发送到用户空间而丢弃的数据包数量。 如果此-
    计数器不为零,请尝试增加netlink缓冲区大小。 在应用程序方面,如果netlink消息丢失,您将看到数据包ID的差距。
  • id序列:最后一个包的包ID
  • 1

经常问的问题

libnetfilter_queue和多线程
libnetfilter_queue取决于发送到套接字的消息。 send / recv操作需要通过锁保护,以避免并发写入。 这意味着nfq_set_verdict2和nfq_handle_packet函数需要通过锁定机制来保护。

接收消息和发送消息是完全独立的操作,不共享任何内存。 特别是判决只使用包索引作为信息。 因此,只要锁定不同,线程就可以为队列中的任何数据包进行判定。

数据包重新排序

使用NFQUEUE可以轻松地进行数据包重新排序,因为可以对任何已排队的数据包进行判定操作。 尽管有一点需要考虑的是排队数据包的内核实现是通过链表来实现的。 所以判断不在列表开头的数据包是昂贵的(最老的数据包是第一个)。

libnetfilter_queue和零拷贝

由于内核和用户空间之间的通信基于发送到netlink套接字的消息,因此不存在零拷贝等问题。 Patrick McHardy已经启动了netlink的内存映射实现,因此将来可能会有零拷贝。

你可能感兴趣的:(使用NFQUEUE和libnetfilter_queue)