Switch-Router

Dive into eBPF (3): 虚拟机程序执行的时机

Published at 2019-12-02 | Last Update 2019-12-02

在 Dive into eBPF(2)中,我们通过例子了解了 eBPF 程序是如何被 load 到内核的,而本文将回答上一篇文章中的第 2 个问题,即:

Q2: 内核指令何时执行,执行的上下文是什么?

先给出答案:eBPF 程序指令都是在内核的特定 Hook 点执行,不同类型的程序有不同的钩子,有不同的上下文

将指令 load 到内核时,内核会创建 bpf_prog 存储指令,但只是第一步,成功运行这些指令还需要完成以下两个步骤:

  • 将 bpf_prog 与内核中的特定 Hook 点关联起来,也就是将程序挂到钩子上。
  • 在 Hook 点被访问到时,取出 bpf_prog,执行这些指令。

将 bpf_prog 设置到 Hook 点

不同类型的 eBPF 程序在内核中有不同的 Hook 点,它们的设置方式也有些区别。SOCKET FILTER 类型 eBPF 程序通过 SO_ATTACH_BPF 选项完成设置(见Dive into eBPF 1) ,而另一种 XDP 类型的 eBPF 程序,则通过 Netlink 的方式设置 Hook 点。

XDP (eXpress Data Path) 是一个快速的报文处理路径(DataPath), 它的 Hook 点在网卡的驱动程序中(协议栈下面).如下图所示, XDP 可以用来做 DDoS 攻击解决方案, 由于处理时机提前了,它比内核自己的 Netfilter 更加高效.

在 Dive into eBPF 2 中我们知道,每一个 load 到内核的 eBPF 程序都有一个 fd 会返回给用户,它对应一个 bpf_prog。 XDP 程序设置 Hook 点的方式就是将这个 fd 与 一个网卡联系起来,通过 Netlink 消息告诉内核。我们可以通过 sample 目录下的 xdp1_user.c 看到这个过程

int main(int argc, char **argv)
{
    // code omitted ...
    bpf_set_link_xdp_fd(ifindex, prog_fd, xdp_flags)
    // code omitted ...
}

其中 ifindex 为网卡的标识,而 prog_fd 为 load 的 eBPF 程序时返回的 fd。

int bpf_set_link_xdp_fd(int ifindex, int fd, __u32 flags)
{
   // code omitted ...
   nla->nla_type = NLA_F_NESTED | IFLA_XDP;
   // code omitted ...
   nla_xdp->nla_type = IFLA_XDP_FD;  
   // code omitted ...

bpf_set_link_xdp_fd 打包 Netlink 消息,消息类型为 IFLA_XDP,子类型为 IFLA_XDP_FD, 表示要关联 bpf_prog

内核收到该 Netlink 消息后, 根据消息类型,最终调用到 dev_change_xdp_fd

do_setlink
{
    // code omitted ...
    if (tb[IFLA_XDP]) {
        // code omitted ...
        if (xdp[IFLA_XDP_FD]) {
			err = dev_change_xdp_fd(dev, extack,
						nla_get_s32(xdp[IFLA_XDP_FD]),
						xdp_flags);
    }
}

dev_change_xdp_fd 意为为 dev 关联一个 XDP 程序的 fd。它使用网卡设备驱动程序的 do_bpf 方法,进行 XDP 程序的安装

int dev_change_xdp_fd(struct net_device *dev, struct netlink_Ext_Ack *extack, int fd, u32 flags)
{
    const struct net_device_ops *ops = dev->netdev_ops;
    bpf_op = bpf_chk = ops->ndo_bpf;
    ......
    prog = bpf_prog_get_type_dev(fd, BPF_PROG_TYPE_XDP, bpf_op == ops->ndo_bpf);
    ......                      
    dev_xdp_install(dev, bpf_op, extack, flags, prog); // 调用设备驱动的 ndo_bpf 方法,命令为 XDP_SETUP_PROG
}

每个支持 XDP 的网卡都有自己的 ndo_bpf 实现,以 Intel i40e 为例,其实现为 i40e_xdp

static const struct net_device_ops i40e_netdev_ops = {
    // code omitted ...
    .ndo_bpf		= i40e_xdp,
}

static int i40e_xdp(struct net_device *dev,
		    struct netdev_bpf *xdp)
{
	struct i40e_netdev_priv *np = netdev_priv(dev);
	struct i40e_vsi *vsi = np->vsi;

	switch (xdp->command) {
	case XDP_SETUP_PROG:
		return i40e_xdp_setup(vsi, xdp->prog);  // add/remove an XDP program
    // code omitted ...           

对 i40e 网卡来说,安装 eBPF 程序即是将 bpf_prog 记录到 vsi 和 vsi->rx_rings 上。

static int i40e_xdp_setup(struct i40e_vsi *vsi, struct bpf_prog *prog)
{
    // code omitted ...
    old_prog = xchg(&vsi->xdp_prog, prog);

    // code omitted ...
	for (i = 0; i < vsi->num_queue_pairs; i++)
		WRITE_ONCE(vsi->rx_rings[i]->xdp_prog, vsi->xdp_prog);
}

运行 Hook 点上设置的 eBPF 程序

设备驱动程序在得到报文时,就会看是否安装过 eBPF 程序,如果有,则运行它,返回运行的结果 (如 PASS 还是 DROP)


i40e_clean_rx_irq
 |
 |- i40e_run_xdp

static struct sk_buff *i40e_run_xdp(struct i40e_ring *rx_ring, struct xdp_buff *xdp)
{
    xdp_prog = READ_ONCE(rx_ring->xdp_prog);
    act = bpf_prog_run_xdp(xdp_prog, xdp);   // 运行 eBPF 程序
	switch (act) {
	    case XDP_PASS:
		    break;
	    // code omitted ...
	    case XDP_DROP:
		    result = I40E_XDP_CONSUMED;
		break;
	}
    // code omitted ...
}

运行 eBPF 程序就是使用 BPF_PROG_RUN,对于 XDP 类型的程序来说,其参数除了指令(prog->insnsi)外,就是报文(struct xdp_buff* xdp )


#define BPF_PROG_RUN(filter, ctx)  (*(filter)->bpf_func)(ctx, (filter)->insnsi)

static u32 bpf_prog_run_xdp(const struct bpf_prog *prog, struct xdp_buff *xdp)
{
	return BPF_PROG_RUN(prog, xdp);
}