浅析suricata源码(2)——AF_XDP_DEV模式包接收分析

77 阅读5分钟

简要分析一下AF_XDP在suricata中应用情况。

运行分析

AF_XDP_DEV运行模式主要用于IDS的情况下进行流量捕获。执行代码运行

sudo suricata --af-xdp=ens224

执行之后发现该网口无法进行正常通信,且tcpdump无法进行抓包。无法抓包主要是因为AF_PACKET的抓包点在XDP后面,程序在XDP这个hook点将流量全部做重定向处理,就导致后面的通信和抓包全部失效,这会在后面代码中进一步验证。

可以参考经典的linux流量路径图:

image.png

XDP也是一种eBPF技术,这边就不做详述。那就可以通过bpftool等工具对其进行分析。

查看加载的eBPF程序

#sudo bpftool prog list

81: xdp  name xdp_dispatcher  tag 4d7e87c0d30db711  gpl
        loaded_at 2025-02-07T10:11:56+0800  uid 0
        xlated 184B  jited 151B  memlock 4096B  map_ids 20
        btf_id 117
90: ext  name xsk_def_prog  tag 8f9c40757cb0a6a2  gpl
        loaded_at 2025-02-07T10:11:56+0800  uid 0
        xlated 96B  jited 73B  memlock 4096B  map_ids 24,23
        btf_id 120
        pids Suricata-Main(1619)

可以看到一个xdp程序和一个ext程序。这里特别注意一下这个ext程序,该程序不会直接运行,是用于动态扩展另一个 BPF 程序。这点可以查看XDP的挂载情况也可以看出来:

#sudo bpftool net show
xdp:
ens224(4) driver id 81

tc:

flow_dissector:

netfilter:

在上面可以看到xdp只挂载一个id为81的程序,该程序就是xdp_dispatcher。详细信息可以参照官方文档docs.ebpf.io/linux/progr…

接下来查看实际代码。

代码分析

模块注册

/* af-xdp */
TmModuleReceiveAFXDPRegister();
TmModuleDecodeAFXDPRegister();

相关代码主要集中在src/source-af-xdp.c这个文件中

接收初始化

ReceiveAFXDPThreadInit->AFXDPSocketCreation

static TmEcode AFXDPSocketCreation(AFXDPThreadVars *ptv)
{

    if (ConfigureXSKUmem(ptv) != TM_ECODE_OK) {
        SCReturnInt(TM_ECODE_FAILED);
    }

    if (InitFillRing(ptv, NUM_FRAMES * 2) != TM_ECODE_OK) {
        SCReturnInt(TM_ECODE_FAILED);
    }

    /* Open AF_XDP socket */
    if (OpenXSKSocket(ptv) != TM_ECODE_OK) {
        SCReturnInt(TM_ECODE_FAILED);
    }

    if (ConfigureBusyPolling(ptv) != TM_ECODE_OK) {
        SCLogWarning("Failed to configure busy polling"
                     " performance may be reduced.");
    }
    //……
}

AFXDPSocketCreation中比较重要的几个步骤

  1. ConfigureXSKUmem

ConfigureXSKUmem->xsk_umem__create该步骤主要用于创建一个用户空间内存(UMEM, User-Space Memory)对象。

简单追一下libxdp源码,主要代码在于

umem->fd = fd > 0 ? fd : socket(AF_XDP, SOCK_RAW, 0);
err = setsockopt(umem->fd, SOL_XDP, XDP_UMEM_REG, &mr, mr_size);
err = xsk_create_umem_rings(umem, umem->fd, fill, comp);

内核态的动作在这里就不深究了。

  1. InitFillRing

该函数主要用来初始化上一步申请的UMEM

  1. OpenXSKSocket->xsk_socket__create 打开一个XSK套接字以进行数据包接收,XSK套接字就是一个AF_XDP套接字的封装。
eBPF程序

在初始化的时候会加载两个eBPF程序:xdp_dispatcher和xsk_def_prog。

可以参考源代码xdp-dispatcher.c文件,但在源码中是一个需要预处理的m4文件,在make之后可以看到

SEC("xdp")
int xdp_dispatcher(struct xdp_md *ctx)
{
        __u8 num_progs_enabled = conf.num_progs_enabled;
        int ret;

        if (num_progs_enabled < 1)
                goto out;
        ret = prog0(ctx);
        if (!((1U << ret) & conf.chain_call_actions[0]))
                return ret;

        if (num_progs_enabled < 2)
                goto out;
        ret = prog1(ctx);
//……
        if (num_progs_enabled < 11)
                        goto out;
        ret = compat_test(ctx);
out:
        return XDP_PASS;
}

__attribute__ ((noinline))
int prog0(struct xdp_md *ctx) {
        volatile int ret = XDP_DISPATCHER_RETVAL;

        if (!ctx)
          return XDP_ABORTED;
        return ret;
}

顾名思义,该程序是一个调度器程序,初看代码好像没干什么事,但参照上文所说的EXT替代机制,是将prog0函数用xsk_def_prog替代了。

/* This is the program for post 5.3 kernels. */
SEC("xdp")
int xsk_def_prog(struct xdp_md *ctx)
{
	/* Make sure refcount is referenced by the program */
	if (!refcnt)
		return XDP_PASS;

	/* A set entry here means that the corresponding queue_id
	 * has an active AF_XDP socket bound to it.
	 */
	return bpf_redirect_map(&xsks_map, ctx->rx_queue_index, XDP_PASS);
}

xsk_def_prog里面最重要的就是bpf_redirect_map这个函数了,这个函数负责网络包重定向,详细可参考官方文档docs.ebpf.io/linux/helpe…

packet的接收

主要逻辑在static TmEcode ReceiveAFXDPLoop(ThreadVars *tv, void *data, void *slot)这个函数的wile循环之中

while (1) {
   //主要逻辑
   //从接收环(RX ring)中获取可用数据包数量 `rcvd`
    rcvd = xsk_ring_cons__peek(&ptv->xsk.rx, ptv->xsk.busy_poll_budget, &idx_rx);
    //…………
    // 确保 UMEM 的填充队列(FQ)有足够空间接收新数据包描述符
    uint32_t res = xsk_ring_prod__reserve(&ptv->umem.fq, rcvd, &idx_fq);
    while (res != rcvd) {
        StatsIncr(ptv->tv, ptv->capture_afxdp_failed_reads);
        ssize_t ret = WakeupSocket(ptv);
        if (ret < 0) {
            SCLogWarning("recv failed with retval %ld", ret);
            AFXDPSwitchState(ptv, AFXDP_STATE_DOWN);
            continue;
        }
        res = xsk_ring_prod__reserve(&ptv->umem.fq, rcvd, &idx_fq);
    }

    gettimeofday(&ts, NULL);
    ptv->pkts += rcvd;
    //分配或复用数据包内存
    for (uint32_t i = 0; i < rcvd; i++) {
        p = PacketGetFromQueueOrAlloc();
        //…………

        uint64_t addr = xsk_ring_cons__rx_desc(&ptv->xsk.rx, idx_rx)->addr;
        uint32_t len = xsk_ring_cons__rx_desc(&ptv->xsk.rx, idx_rx++)->len;
        uint64_t orig = xsk_umem__extract_addr(addr);
        addr = xsk_umem__add_offset_to_addr(addr);

        uint8_t *pkt_data = xsk_umem__get_data(ptv->umem.buf, addr);

        ptv->bytes += len;

        p->afxdp_v.fq_idx = idx_fq++;
        p->afxdp_v.orig = orig;
        p->afxdp_v.fq = &ptv->umem.fq;
        //获取packet数据,这里使用了零拷贝技术,没有进行copy动作
        PacketSetData(p, pkt_data, len);
        //将数据包送入 Suricata 的处理逻辑(如协议解析、规则匹配),该部分之后详细说明
        if (TmThreadsSlotProcessPkt(ptv->tv, ptv->slot, p) != TM_ECODE_OK) {
            TmqhOutputPacketpool(ptv->tv, p);
            SCReturnInt(EXIT_FAILURE);
        }
    }
    //处理过后的packet进行释放
    xsk_ring_prod__submit(&ptv->umem.fq, rcvd);
    xsk_ring_cons__release(&ptv->xsk.rx, rcvd);
    //……
}

/**
 * \brief Set data for Packet and set length when zero copy is used
 *
 *  \param Pointer to the Packet to modify
 *  \param Pointer to the data
 *  \param Length of the data
 */
inline int PacketSetData(Packet *p, const uint8_t *pktdata, uint32_t pktlen)
{
    SET_PKT_LEN(p, (size_t)pktlen);
    if (unlikely(!pktdata)) {
        return -1;
    }
    // ext_pkt cannot be const (because we sometimes copy)
    p->ext_pkt = (uint8_t *) pktdata;
    p->flags |= PKT_ZERO_COPY;

    return 0;
}