简要分析一下AF_XDP在suricata中应用情况。
运行分析
AF_XDP_DEV运行模式主要用于IDS的情况下进行流量捕获。执行代码运行
sudo suricata --af-xdp=ens224
执行之后发现该网口无法进行正常通信,且tcpdump无法进行抓包。无法抓包主要是因为AF_PACKET的抓包点在XDP后面,程序在XDP这个hook点将流量全部做重定向处理,就导致后面的通信和抓包全部失效,这会在后面代码中进一步验证。
可以参考经典的linux流量路径图:
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中比较重要的几个步骤
- 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);
内核态的动作在这里就不深究了。
- InitFillRing
该函数主要用来初始化上一步申请的UMEM
- 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;
}