【作者主页】只道当时是寻常
【专栏介绍】Suricata入侵检测。专注网络、主机安全,欢迎关注与评论。
1. 概要
👋 本文聚焦于 Suricata 7.0.10 版本源码,深入剖析其 NFQ(Netfilter Queue)模式的实现原理。通过系统性拆解初始化阶段的配置流程、数据包监听机制的构建逻辑,以及数据包处理的具体步骤,从源码层面揭示 NFQ 模式如何协同各模块实现高效网络流量监控与安全检测,旨在为网络安全领域的开发者与研究人员提供全面且细致的技术参考。
2. Suricata NFQ
2.1 NFQ初始化
Suricata 的 NFQ 模块启动初始化时,NFQ 数据包接收模块以线程运行。线程启动前,需执行初始化操作,ReceiveNFQThreadInit 是初始化入口,NFQInitThread 函数通过 libnetfilter_queue 库接口,完成 NFQ 句柄和配置相关操作
static TmEcode NFQInitThread(NFQThreadVars *t, uint32_t queue_maxlen)
{
struct timeval tv;
int opt;
NFQQueueVars *q = NFQGetQueue(t->nfq_index);
if (q == NULL) {
SCLogError("no queue for given index");
return TM_ECODE_FAILED;
}
SCLogDebug("opening library handle");
q->h = nfq_open();
if (q->h == NULL) {
SCLogError("nfq_open() failed");
return TM_ECODE_FAILED;
}
...
/* pass the thread memory as a void ptr so the
* callback function has access to it. */
q->qh = nfq_create_queue(q->h, q->queue_num, &NFQCallBack, (void *)t);
if (q->qh == NULL) {
SCLogError("nfq_create_queue failed");
return TM_ECODE_FAILED;
}
SCLogDebug("setting copy_packet mode");
/* 05DC = 1500 */
//if (nfq_set_mode(nfq_t->qh, NFQNL_COPY_PACKET, 0x05DC) < 0) {
if (nfq_set_mode(q->qh, NFQNL_COPY_PACKET, 0xFFFF) < 0) {
SCLogError("can't set packet_copy mode");
return TM_ECODE_FAILED;
}
...
/* set netlink buffer size to a decent value */
nfnl_rcvbufsiz(nfq_nfnlh(q->h), queue_maxlen * 1500);
SCLogInfo("setting nfnl bufsize to %" PRId32 "", queue_maxlen * 1500);
q->nh = nfq_nfnlh(q->h);
q->fd = nfnl_fd(q->nh);
NFQMutexInit(q);
/* Set some netlink specific option on the socket to increase
...
return TM_ECODE_OK;
}
上面是NFQInitThread 函数代码实现,其中非必要部分使用"..."进行了省略。着重关注四个函数,分别是nfq_open、nfq_create_queue、nfq_set_mode和nfnl_rcvbufsiz。
🌲 struct nfq_handle *nfq_open(void);
该函数是 Netfilter Queue (NFQ) 库中的一个函数,用于创建一个新的 Netfilter Queue 句柄。这个句柄是后续所有 NFQ 操作的基础。
🌲 struct nfq_q_handle *nfq_create_queue(struct nfq_handle*h, uint16_t num, nfq_callback *cb, void *data);
该函数用于在指定的NFQ句柄上创建一个新的队列。这个队列将用于接收来自内核的网络数据包,并通过用户提供的回调函数进行处理。
struct nfq_handle *h:通过nfq_open()函数返回的nfq句柄;
uint16_t num:队列(Queue)索引号;
nfq_callback *cb:回调函数指针,用于处理从内核接收到的数据包。
void *data:用户自定义数据指针,会传递给回调函数。
🌲 int nfq_set_mode(struct nfq_q_handle *qh, uint8_t mode, unsigned int len);
该函数用于设置 NFQ 队列的工作模式。它决定了内核如何处理传递给用户空间的网络数据包。其支持的工作模式包含:
struct nfq_q_handle *qh:指向Netfilter 队列的句柄;
uint8_t mode:工作模式;
NFQNL_COPY_NONE: 不复制数据包内容;
NFQNL_COPY_META: 仅复制数据包元数据;
NFQNL_COPY_PACKET: 复制整个数据包内容(Suricata中设置为此模式);
uint32_t len:当 mode 设置为 NFQNL_COPY_PACKET 时,该参数指定要复制到用户空间的数据包的最大字节数。如果设置为一个较大的值(例如 0xFFFF,Suricata中设置的此值),则会尝试复制整个数据包。
🌲 unsigned int nfnl_rcvbufsiz(const struct nfnl_handle *h, unsigned int size);
该函数是 Netfilter Netlink 库中的一个函数,用于设置 Netlink 套接字的接收缓冲区大小。
struct nfnl_handle *h: Netfilter Netlink 句柄,可通过nfq_nfnlh()函数将nfq句柄转换成 Netfilter Netlink 句柄。
unsigned int size:接收缓冲区的大小,以字节为单位。
🌲 struct nfnl_handle *nfq_nfnlh(struct nfq_handle *h);
该函数是 Netfilter Queue (NFQ) 库中的一个函数,用于从 NFQ 句柄中获取 Netfilter Netlink 句柄。这个句柄用于与内核的 Netfilter 子系统进行通信。
struct nfq_handle *h:通过nfq_open()函数返回的nfq句柄;
🌲 int nfnl_fd(struct nfnl_handle *h);
该函数是 Netfilter Netlink 库中的一个函数,用于从 Netfilter Netlink 句柄中获取底层的文件描述符。这个文件描述符可以用于直接操作 Netlink 套接字,例如读写网络数据包等。
struct nfnl_handle *h:Netfilter Netlink 句柄;
🌈 总结:初始化阶段借助 Netfilter Queue 和 Netfilter Netlink 库创建 Netlink 和 Netfilter Queue 句柄,配置队列长度、收包方式与大小,获取 Netlink 套接字以备后续收包。
2.2 NFQ监听数据包
NFQ 数据包接收线程启动,以 ReceiveNFQLoop 函数为执行函数。此函数内含 while 循环,持续调用 NFQRecvPkt 函数。NFQRecvPkt 中用 recv 监听 Netlink 套接字,接收到数据包时调用 Netfilter Queue (NFQ) 库的 nfq_handle_packet 处理。
/**
* \brief NFQ function to get a packet from the kernel
*
* \note separate functions for Linux and Win32 for readability.
*/
static void NFQRecvPkt(NFQQueueVars *t, NFQThreadVars *tv)
{
int ret;
int flag = NFQVerdictCacheLen(t) ? MSG_DONTWAIT : 0; // cz:MSG_DONTWAIT: 非阻塞模式,如果没有数据可接收,立即返回。
int rv = recv(t->fd, tv->data, tv->datalen, flag);
if (rv < 0) {
if (errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN) {
/* no error on timeout */
if (flag)
NFQVerdictCacheFlush(t);
/* handle timeout */
TmThreadsCaptureHandleTimeout(tv->tv, NULL);
} else {
#ifdef COUNTERS
NFQMutexLock(t);
t->errs++;
NFQMutexUnlock(t);
#endif /* COUNTERS */
}
} else if(rv == 0) {
SCLogWarning("recv got returncode 0");
} else {
#ifdef DBG_PERF
if (rv > t->dbg_maxreadsize)
t->dbg_maxreadsize = rv;
#endif /* DBG_PERF */
NFQMutexLock(t);
if (t->qh != NULL) {
ret = nfq_handle_packet(t->h, tv->data, rv);
} else {
SCLogWarning("NFQ handle has been destroyed");
ret = -1;
}
NFQMutexUnlock(t);
if (ret != 0) {
SCLogDebug("nfq_handle_packet error %"PRId32, ret);
}
}
}
上面是 NFQRecvPkt 函数代码实现。
🌲 int nfq_handle_packet(struct nfq_handle *h, char *buf, int len);
该函数是 Netfilter Queue (NFQ) 库中的函数,用于处理从内核接收到的数据包。它负责解析 Netlink 消息并调用注册的回调函数来处理数据包。在3.1 NFQ初始化章节提到在初始化时调用nfq_create_queue函数注册了NFQCallBack回调函数,而在Suricata中就是依赖该回调函数解析处理数据包的。
struct nfq_handle *h:通过nfq_open()函数返回的nfq句柄;
char *buf:指向包含 Netlink 消息的缓冲区的指针,即recv函数接收到的数据;
int len:缓冲区中数据的长度;
🌈 总结:在NFQRecvPkt 函数中Suricata通过Netlink 套接字内核发送的消息,当接收到消息后调用NFQCallBack回调函数对消息进行处理,从而实现数据的监听功能。
2.3 NFQ处理数据包
NFQ 数据包的接收与处理通常由不同线程执行,也可能由一个线程完成。3.2 章节指出,nfq_handle_packet 函数会把从 Netlink 套接字接收的数据传递给 nfq_create_queue 注册的回调函数 NFQCallBack。在 NFQCallBack 里,先通过 NFQSetupPkt 将数据转为 Packet 格式,再经 TmThreadsSlotProcessPkt 调用 DecodeNFQ 处理数据包。
/**
* \brief Decode a packet coming from NFQ
*/
TmEcode DecodeNFQ(ThreadVars *tv, Packet *p, void *data)
{
IPV4Hdr *ip4h = (IPV4Hdr *)GET_PKT_DATA(p);
IPV6Hdr *ip6h = (IPV6Hdr *)GET_PKT_DATA(p);
DecodeThreadVars *dtv = (DecodeThreadVars *)data;
BUG_ON(PKT_IS_PSEUDOPKT(p));
DecodeUpdatePacketCounters(tv, dtv, p);
if (IPV4_GET_RAW_VER(ip4h) == 4) {
if (unlikely(GET_PKT_LEN(p) > USHRT_MAX)) {
return TM_ECODE_FAILED;
}
SCLogDebug("IPv4 packet");
DecodeIPV4(tv, dtv, p, GET_PKT_DATA(p), GET_PKT_LEN(p));
} else if (IPV6_GET_RAW_VER(ip6h) == 6) {
if (unlikely(GET_PKT_LEN(p) > USHRT_MAX)) {
return TM_ECODE_FAILED;
}
SCLogDebug("IPv6 packet");
DecodeIPV6(tv, dtv, p, GET_PKT_DATA(p), GET_PKT_LEN(p));
} else {
SCLogDebug("packet unsupported by NFQ, first byte: %02x", *GET_PKT_DATA(p));
}
PacketDecodeFinalize(tv, dtv, p);
return TM_ECODE_OK;
}
上面是DecodeNFQ函数实现,可以看到,在这个函数中根据报文网络层协议版本执行不同的解码函数。假若是IPv4则执行DecodeIPV4函数,假若是IPv6则执行DecodeIPV6函数。这也是为什么说Suricata的NFQ模式是基于网络层实现的。
2.4 总结
综上所述,3.1、3.2 和 3.3 章节完整呈现了 Suricata 从初始化、监听数据包到接收并处理数据包的全流程。
3. 参考资料
Suricata 7.0.10 版本源码。