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);
}