【问题标题】:netfilter_queue spurious packetsnetfilter_queue 虚假数据包
【发布时间】:2014-01-24 02:26:09
【问题描述】:

我正在使用 netfilter 队列库实现用户空间防火墙。我使用nfq_fd() 获得了队列的文件描述符,因此我可以调用recv(fd, recv_buf, BUFFERSIZE, MSG_DONTWAIT) 来获取数据包数据而不会阻塞。但有时recv() 每次调用它时都会开始返回 52 字节的数据包。如果我检查iptables -nvL INPUT 的输出,数据包的数量不会增加,因此它们实际上并不是从网络发送的。 Edit3:nfq_handle_packet() 在我传递其中一个奇数数据包时返回 -1,它从不触发回调函数,因此我无法获取数据包 ID 或返回判决。

为什么 recv() 给我这些奇怪的数据包?

编辑1:

这些数据包并不完全相同,但它们具有相似的结构。也有一些重复。这是其中一些的十六进制转储:

0000   34 00 00 00 02 00 00 00  00 00 00 00 BE 4E 00 00   4............N..
0010   FE FF FF FF 20 00 00 00  01 03 01 00 00 00 00 00   .... ...........
0020   00 00 00 00 00 00 00 00  0C 00 02 00 00 00 00 01   ................
0030   01 00 00 00                                        ....

0000   34 00 00 00 02 00 00 00  00 00 00 00 5B 69 00 00   4...........[i..
0010   FE FF FF FF 20 00 00 00  01 03 01 00 00 00 00 00   .... ...........
0020   00 00 00 00 00 00 00 00  0C 00 02 00 00 00 00 01   ................
0030   00 00 01 95                                        ....

0000   34 00 00 00 02 00 00 00  00 00 00 00 5B 69 00 00   4...........[i..
0010   FE FF FF FF 20 00 00 00  01 03 01 00 00 00 00 00   .... ...........
0020   00 00 00 00 00 00 00 00  0C 00 02 00 00 00 00 01   ................
0030   00 00 01 95                                        ....

编辑2:

代码非常简陋,只是根据我找到的一些 netfilter_queue 教程进行了调整。

#include <linux/netfilter.h>
#include <libnetfilter_queue/libnetfilter_queue.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <syslog.h>

#define BUFFERSIZE 500

int main()
{
   struct nfq_handle *h;
   struct nfq_q_handle *qh;
   struct my_nfq_data msg;
   int fd;
   unsigned char recv_buf[BUFFERSIZE];
   int action;

   if ((stat("/proc/net/netfilter/nfnetlink_queue", &fbuf) < 0) && (errno == ENOENT))
     {
        fprintf(stderr, "Please make sure nfnetlink_queue is installed, or that you have\ncompiled a kernel with the Netfilter QUEUE target built in.\n");
        exit(EXIT_FAILURE);
     }

   openlog("packetbl", LOG_PID, "local6");

   if ((h = nfq_open()) == 0)
     {    
        syslog(LOG_ERR, "Couldn't open netlink connection: %s", strerror(errno));
        exit(EXIT_FAILURE);
     }    

   nfq_unbind_pf(h, AF_INET);
   if ((nfq_bind_pf(h, AF_INET) < 0))
     {    
        syslog(LOG_ERR, "Couldn't bind to IPv4: %s", strerror(errno));
     }    

   nfq_unbind_pf(h, AF_INET6);
   if ((nfq_bind_pf(h, AF_INET6) < 0))
     {    
        syslog(LOG_ERR, "Couldn't bind to IPv6: %s", strerror(errno));
     }    

   if ((qh = nfq_create_queue(h, 0, &callback, &msg)) == NULL)
     {    
        syslog(LOG_ERR, "Couldn't create nfq: %s", strerror(errno));
        exit(EXIT_FAILURE);
     }    

   if ((nfq_set_mode(qh, NFQNL_COPY_PACKET, BUFFERSIZE)) == -1)
     {    
        syslog(LOG_ERR, "nfq_set_mode error: %s", strerror(errno));
        if (errno == 111) 
          {    
             syslog(LOG_ERR, "try loading the nfnetlink_queue module");
          }    
        exit(EXIT_FAILURE);
     }    

   fd = nfq_fd(h);

   while(1)
     {

        /* Up here I print some statistics on packets allowed and blocked.
           It prints on a schedule, so the recv() call has to be non-blocking
           or else the statistics would only print out when there's a packet. */

        recv_return_code = recv(fd, recv_buf, BUFFERSIZE, MSG_DONTWAIT); //nonblocking

        if (recv_return_code < 0)
          {
            if (errno == EAGAIN ||
                errno == EWOULDBLOCK)
              {
                nanosleep(&times,NULL);
              }
            else
              {
                syslog(LOG_ERR, "recv failed: %s", strerror(errno));
              }
            continue;
          }

        printf("received %d bytes\n", recv_return_code);

        /* when nfq_handle_packet() succeeds, it triggers the callback
           which puts the packet data into a global variable "msg" */
        if (nfq_handle_packet(h, recv_buf, recv_return_code) != 0)
          {
            syslog(LOG_ERR, "couldn't handle packet");
          }

        action = packet_check_ip(msg);

        pbl_set_verdict(qh, ntohl(msg.header.packet_id), action);
     }
}

编辑 4:

我使用 scapy 作为流量生成器。如果我一次只发送一个数据包,那么我会收到 0 或 1 个虚假数据包,然后它就会停止。这是 strace 的输出:

recvfrom(3, "x\0\0\0\0\3\0\0\0\0\0\0\0\0\0\0\n\0\0\0\v\0\1\0\0\0\0\6\206\335\1\0\10\0\5\0\0\0\0\2\20\0\t\0\0\6\261\201\0\f)7Z\22\0\0@\0\n\0`\0\0\0\0\24\6@&\6\364\0\10\0\0\0\0\0\0\0\0\0p\5&\6\364\0\10\0\0\0\0\0\0\0\0\0p\4\0\24\0\31\0\0\0\0\0\0\0\0P\2 \0k\236\0\0", 9216, MSG_DONTWAIT, NULL, NULL) = 120
sendto(4, "<182>Jan 13 10:51:20 packetbl[8785]: [Found in cache (accept)] [2606:f400:800::7005,20,25]", 90, MSG_NOSIGNAL, NULL, 0) = 90
sendmsg(3, {msg_name(12)={sa_family=AF_NETLINK, pid=0, groups=00000000}, msg_iov(1)=[{" \0\0\0\1\3\1\0\0\0\0\0\0\0\0\0\0\0\0\0\f\0\2\0\0\0\0\1\0\0\0\6", 32}], msg_controllen=0, msg_flags=0}, 0) = 32
recvfrom(3, "x\0\0\0\0\3\0\0\0\0\0\0\0\0\0\0\n\0\0\0\v\0\1\0\0\0\0\7\206\335\1\0\10\0\5\0\0\0\0\2\20\0\t\0\0\6\261\201\0\f)7Z\22\0\0@\0\n\0`\0\0\0\0\24\6@&\6\364\0\10\0\0\0\0\0\0\0\0\0p\1&\6\364\0\10\0\0\0\0\0\0\0\0\0p\4\0\24\0\31\0\0\0\0\0\0\0\0P\2 \0k\242\0\0", 9216, MSG_DONTWAIT, NULL, NULL) = 120
futex(0x60c984, FUTEX_CMP_REQUEUE_PRIVATE, 1, 2147483647, 0x607fc0, 8) = 2
futex(0x607fc0, FUTEX_WAKE_PRIVATE, 1)  = 1
sendmsg(3, {msg_name(12)={sa_family=AF_NETLINK, pid=0, groups=00000000}, msg_iov(1)=[{" \0\0\0\1\3\1\0\0\0\0\0\0\0\0\0\0\0\0\0\f\0\2\0\0\0\0\1\7\0\0\0", 32}], msg_controllen=0, msg_flags=0}, 0) = 32
recvfrom(3, "4\0\0\0\2\0\0\0\0\0\0\0Q\"\0\0\376\377\377\377 \0\0\0\1\3\1\0\0\0\0\0\0\0\0\0\0\0\0\0\f\0\2\0\0\0\0\1\7\0\0\0", 9216, MSG_DONTWAIT, NULL, NULL) = 52
sendto(4, "<179>Jan 13 10:51:22 packetbl[8785]: couldn't handle packet", 59, MSG_NOSIGNAL, NULL, 0) = 59
sendmsg(3, {msg_name(12)={sa_family=AF_NETLINK, pid=0, groups=00000000}, msg_iov(1)=[{" \0\0\0\1\3\1\0\0\0\0\0\0\0\0\0\0\0\0\0\f\0\2\0\0\0\0\1\0\0\0\7", 32}], msg_controllen=0, msg_flags=0}, 0) = 32

我可以像转动手指一样快地发送单个数据包,而且它永远不会陷入死亡螺旋。但是,如果我一次发送 4 个数据包,它有时会为每个真实数据包触发一个(或零个)虚假数据包,但有时我会收到无限的虚假数据包。如果我发送很多数据包,它总是无限的。

我以前见过一些这种行为,但 Nominal Animal 的回答唤起了我的记忆。如上所示,关于我的代码的一件奇怪的事情是,即使nfq_handle_packet() 失败,我仍然会执行packet_check_ip()pbl_set_verdict()。我认为在这种情况下放置continue; 是有意义的,因为否则我将在msg 变量中处理陈旧数据。 (如果我错了,请纠正我,但这应该与将数据包处理和判决移到回调中具有相同的效果。)但这始终会在 1 个真实数据包之后引发无限的虚假数据包。我也将判决暂时移到回调中,它没有改变任何东西。

那么不知何故,对旧数据调用 set_verdict 有时会阻止无穷大?

哦,这里是pbl_set_verdict() 的代码,如果有人担心它可能会做任何聪明的事情:)

static void pbl_set_verdict(struct nfq_q_handle *qh,
                            uint32_t id,
                            unsigned int verdict)
{  
   nfq_set_verdict(qh, id, verdict, 0, NULL);
}

编辑 5:

我已经编译并运行了与 libnetfilter_queue 一起分发的 nfqnl_test.c 示例,它运行得很好。所以这可能不是库本身的问题。

编辑 6:

现在我到了某个地方 :) 事实证明,在容量过剩的情况下,ntohl() 被调用了两次!而且因为即使nfq_handle_packet 失败,我也在对陈旧数据调用pbl_set_verdict(),它正确地运行了数据,从而产生了正确的效果。这就是为什么当我将pbl_set_verdict() 调用移至回调函数时队列已满的原因——它从未有机会解决由容量过剩情况引起的问题。而且陈旧的数据只包括一些处理过的数据包,所以无论如何最终都会有一堆数据包填满队列。

即使我的程序现在可以运行,我仍然对这些数据包是什么以及为什么它们似乎没有记录在案感到困惑。

【问题讨论】:

  • 您是否尝试过打印数据包?它们都一样吗?
  • 在 Wireshark 中你看不到那些数据包? TCP/UDP?那些包的内容是什么?每个都不一样?
  • 我的疯狂猜测是,您忽略了在回调函数中对某些接收到的数据包设置判断,并且内核吓坏了,快用完了缓冲区可能——我可能弄错了,但我认为内核可能只重新发送配置消息中的数据包 ID 而不是整个数据包。为了得到真正的答案,我们需要详细的信息和足够的代码来重现问题。否则我们都只是在浪费时间。至少,需要一些此类数据包和关键代码行的十六进制转储。
  • 是的,这是一个交叉点。一个多星期以来,我在 SO 上有几个版本的这个问题都没有答案,所以我也将它发送到了邮件列表。

标签: c linux iptables netfilter


【解决方案1】:

将您的代码与 libnetfilter_queue 源中的 example 进行比较。您的代码在处理数据包之后设置判决(假设您的代码中的pbl_set_verdict() 就是这样做的)。该示例在回调函数中设置判定。

我对 netfilter 内部没有足够的信心来确定这是您问题的根本原因,但我确实相信它。

至于使用非阻塞读取,没有必要这样做。相反,让间隔计时器定期触发一个信号(例如,HUP 或像SIGRTMIN+1 这样的实时信号),并为该信号安装一个空的信号处理函数。当信号被传递(到空体处理程序;IGNDFL 将不起作用)时,这会导致任何阻塞 I/O 调用被中断,假设您的进程只有一个线程。如果间隔很长,使用HUP 很有用,因为这样用户可以向外部发送 HUP 以立即打印统计信息。这种方式不会浪费额外的 CPU 时间。

如果您的应用程序使用多个线程,则需要更多的机器。处理程序需要检查源是否是定时器中断(siginfo-&gt;si_code==SI_TIMER),如果是,则使用pthread_sigqueue() 将中断(相同信号)转发到目标线程,除非当前线程是目标线程。通过 netlink 读取消息的线程需要将它们的线程 ID 保存到中断处理程序可以访问它们的位置。 (此外,您的其他代码必须知道errno==EINTR 可能会发生,并且不是错误,除非它们专门阻止了信号。)

换句话说,我希望你的代码更像

/* In case of an error, break out of the following loop.
 * You can either exit, or close and re-establish the netlink
 * and queue.
*/
while(1)
{
    ssize_t bytes;

    /* Read a new netlink message.
       Note: Technically, BUFFERSIZE should be about 65536,
             since each message has a uint16_t message length field.
    */
    bytes = recv(fd, recv_buf, BUFFERSIZE, MSG_DONTWAIT);

    /* C library, or kernel recv() bug?
    */
    if (bytes < (ssize_t)-1 || bytes > (ssize_t)BUFFERSIZE) {
        errno = EIO;
        break; /* out of the while (1) loop */
    }

    /* Netlink closed? Should not occur. */
    if (bytes == (ssize_t)0) {
        errno = 0;
        break; /* No error, just netlink closed. Drop out. */
    }

    /* No message? */
    if (bytes == (ssize_t)-1) {
        if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) {

            /* Print overall statistics.
            */

            continue;
        } else
            break; /* Other errors drop out of the loop. */
    }

    if (nfq_handle_packet(h, recv_buf, bytes)) {
        /* Packet was dropped on the floor.
         * This is a serious problem, so we treat this as EIO.
        */
        errno = EIO;
        break;
    }
}

回调基本上是

static int callback(struct nfq_q_handle *qh,
                    struct nfgenmsg *nfmsg,
                    struct nfq_data *nfa,
                    void *data)
{
    return nfq_set_verdict(qh, id, packet_check_ip(nfmsg), 0, NULL);
}

至于上述多线程,您可以简单地让多个线程同时运行上述循环(显然,使用不同的recv_buf 缓冲区)。然后,接收数据包的线程也会处理它,包括回调。线程安全应该没有问题,除非您自己的代码是非线程安全的。如果线程应该退出,您还可以在 if 子句中的 “打印整体统计信息” 注释之前添加一个检查(针对某些全局 volatile 标志);然后您可以简单地设置标志,并发送信号以更新统计信息,让所有工作线程退出,而不会“在地板上”丢弃任何数据包。

有什么问题吗?

【讨论】:

  • 我通常会尽量避免混合信号和线程 :) 我有多个“工作”线程,但只有一个从队列中读取。如果这个项目还不够复杂,我会将统计信息移到他们自己的线程中,并让 recv() 阻止它想要的一切。也许有一天!
  • 我并不是要忽略您的回答,但我尝试将 nfq_set_verdict() 移动到回调中并遇到了一些非常奇怪的行为。我正在尝试找到一个好的测试用例,然后我会写出结果。
  • @sep332:如果您不将BUFFERSIZE 增加到至少 1600 左右(以太网帧为 1500,加上一些用于 netfilter 的东西),我希望您确实会遇到非常奇怪的行为.您会看到,对于普通以太网数据包,MTU(最大传输单元)为 1500,对于巨型帧,最高为 9216。对于 TCP/IP 和 UDP/IP,堆栈会重建碎片数据包,因此单个 netfilter 消息可能会大得多。因此,我建议设置BUFFERSIZE=65536,看看它是否能解决你看到的奇怪行为。判决肯定属于回调;如果您不同意,请考虑一下。
  • 我在 strace 中运行了该程序,但从未见过 recv() 返回超过 120 个字节。 iptables 毕竟只是发送 SYN 数据包。不过,这不是一个坏主意。我碰到了BUFFERSIZE,但它对行为没有任何影响。
  • @sep332:您是否运行了上游示例(在我的答案开头链接)?这里存在多个问题并非不可想象。最好一一消除尽可能多的来源。如果规范示例有效,那么内核/库端就可以了。否则,问题可能根本不在您的代码中。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-02-25
  • 1970-01-01
  • 1970-01-01
  • 2019-07-03
  • 2020-11-12
相关资源
最近更新 更多