我们都知道写 unittest 是非常必要的,但是在 eBPF 程序里,该如何给 XDP 程序写 unittest 呢?

在 4.12 内核中,引入了 BPF_PROG_TEST_RUN 命令,可以用来给 XDP 和 tc-bpf 等 eBPF 程序写 unittest。

如果给 eBPF Talk: 使用 metadata 将信息从 XDP 传给 AF_XDP 写 unittest,该怎么写呢?

cilium/ebpf 里的 unittest 例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// https://github.com/cilium/ebpf/blob/master/prog_test.go#L93

    buf := internal.EmptyBPFContext
    xdp := sys.XdpMd{
        Data:    0,
        DataEnd: uint32(len(buf)),
    }
    xdpOut := sys.XdpMd{}
    opts := RunOptions{
        Data:       buf,
        Context:    xdp,
        ContextOut: &xdpOut,
    }
    ret, err := prog.Run(&opts)
    testutils.SkipIfNotSupported(t, err)
    if err != nil {
        t.Fatal(err)
    }

    if ret != 0 {
        t.Error("Expected return value to be 0, got", ret)
    }

因为 eBPF Talk: 使用 metadata 将信息从 XDP 传给 AF_XDP 的 XDP 程序里涉及网络包内容的检查,以及使用了 XDP metadata,所以该例子并不足以指导我们写 unittest。

如何获取 XDP 程序里写入的 metadata?

先看一下 RunOptions 的定义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
type RunOptions struct {
    // Program's data input. Required field.
    //
    // The kernel expects at least 14 bytes input for an ethernet header for
    // XDP and SKB programs.
    Data []byte
    // Program's data after Program has run. Caller must allocate. Optional field.
    DataOut []byte

    // ...
}

其中 Data 是提供给 XDP 程序处理的网络包内容,DataOut 是 XDP 程序处理后的网络包内容。

metadata 是否在 DataOut 里呢?我们来看一下 BPF_PROG_TEST_RUN 命令的实现:

1
2
3
4
5
6
7
8
9
__sys_bpf()                                 // ${KERNEL}/kernel/bpf/syscall.c
|-->bpf_prog_test_run()
    |-->ret = prog->aux->ops->test_run(prog, attr, uattr);
     \
      \
       \
        |-->bpf_prog_test_run_xdp()         // ${KERNEL}/net/bpf/test_run.c
            |-->ret = bpf_test_finish(kattr, uattr, xdp.data_meta, sinfo, size, retval, duration);
                |-->copy_to_user(data_out, data, len);

可以看到,BPF_PROG_TEST_RUN 命令的实现,将包括 metadata 的整个网络包的内容拷贝到 data_out 里。

在 unittest 里检查 metadata

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
func preparePacketData() []byte {
    buf := make([]byte, 14+20+8)     // eth + iph + icmph
    be.PutUint16(buf[12:14], 0x0800) // ethertype = IPv4

    iph := buf[14:]
    iph[0] = 0x45 // version = 4, ihl = 5
    iph[9] = 1    // protocol = ICMP

    icmph := iph[20:]
    icmph[0] = 8 // type = ECHO

    return buf
}

func TestXDPProgRun(t *testing.T) {
    ifi, err := netlink.LinkByName("lo")
    if err != nil {
        t.Fatalf("Failed to get device info: %v", err)
    }

    xsk, err := xdp.NewSocket(ifi.Attrs().Index, 0, nil)
    if err != nil {
        t.Fatalf("Failed to new XDP socket: %v", err)
    }
    defer xsk.Close()

    var obj xdpfnObjects
    if err := loadXdpfnObjects(&obj, nil); err != nil {
        t.Fatalf("Failed to load XDP bpf obj: %v", err)
    }
    defer obj.Close()

    // Map is required to be populated before running the program for `bpf_redirect_map`.
    if err := obj.XdpSockets.Put(uint32(0), uint32(xsk.FD())); err != nil {
        t.Fatalf("Failed to update XDP socket bpf map: %v", err)
        return
    }

    data := preparePacketData()
    dataOut := make([]byte, len(data)+4)

    act, err := obj.XdpFn.Run(&ebpf.RunOptions{
        Data:    data,
        DataOut: dataOut,
    })
    if err != nil {
        t.Fatalf("Failed to run XDP bpf prog: %v", err)
    }

    if act != 4 { // XDP_REDIRECT
        t.Fatalf("Expected action %d, got %d", 4, act)
    }

    lat := *(*uint32)(unsafe.Pointer(&dataOut[0]))
    if lat != 200 {
        t.Fatalf("Expected latency %d, got %d", 200, lat)
    }
}

在该代码片段里,需要注意的是:

  1. preparePacketData() 准备好网络包内容。
  2. xdp_sockets bpf map 写入一个 XDP socket fd,否则 bpf_redirect_map 会报错,因为 bpf_redirect_map() 会查询 bpf map 里的值。
  3. XDP 程序的最终结果时 XDP_REDIRECT,并且将 latency 写入 metadata。
  4. DataOut 起始内容是 metadata,后面是网络包内容。

小结

本文介绍了如何给 XDP 程序写 unittest,以及如何在 unittest 里检查 XDP 程序写入的 metadata。