在 XDP 程序运行起来后,特别是在生产环境中,有没有办法去观察它的运行情况呢?特别是 XDP 程序的最终结果。

将问题泛化一下,即有没有办法去 trace bpf 程序?

TL;DR 有,却没有,得看具体是那种 bpf 程序。不过,trace XDP 程序是可以的。

trace XDP 程序的 demo

demo 效果如下:

1
2
3
4
5
6
7
8
9
# ./fentry_fexit-xdp --device enp0s8
2023/07/15 06:56:51 Attached xdp to enp0s8
2023/07/15 06:56:51 Attached fentry(xdp)
2023/07/15 06:56:51 Attached fexit(xdp)
2023/07/15 06:56:51 Listening events...
2023/07/15 06:56:58 Tracing packet: 192.168.1.12 -> 192.168.1.138 (fentry)
2023/07/15 06:56:58 Tracing packet: 192.168.1.12 -> 192.168.1.138 (fexit: XDP_PASS)
2023/07/15 06:56:59 Tracing packet: 192.168.1.12 -> 192.168.1.138 (fentry)
2023/07/15 06:56:59 Tracing packet: 192.168.1.12 -> 192.168.1.138 (fexit: XDP_PASS)

其中使用的 trace 手段是 fentryfexit

demo 使用的 bpf 代码如下:

 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
typedef struct event {
    __be32 saddr, daddr;
    __u8 is_fexit;
    __u8 verdict;
    __u16 pad;
} __attribute__((packed)) event_t;

struct {
    __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
} events SEC(".maps");

static __always_inline void
handle_xdp(void *ctx, struct xdp_buff *xdp, int verdict, bool is_fexit)
{
    struct ethhdr *eth = (void *)(long)BPF_CORE_READ(xdp, data);
    struct iphdr *ip = (void *)(eth + 1);
    if ((void *)(ip + 1) > (void *)(long)BPF_CORE_READ(xdp, data_end))
        return;

    if (BPF_CORE_READ(eth, h_proto) != bpf_htons(ETH_P_IP))
        return;

    if (BPF_CORE_READ(ip, protocol) != IPPROTO_ICMP)
        return;

    event_t ev = {};
    ev.saddr = BPF_CORE_READ(ip, saddr);;
    ev.daddr = BPF_CORE_READ(ip, daddr);;
    ev.is_fexit = is_fexit;
    ev.verdict = (__u8)verdict;

    bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &ev, sizeof(ev));
}

SEC("fentry/xdp")
int BPF_PROG(fentry_xdp, struct xdp_buff *xdp)
{
    handle_xdp(ctx, xdp, 0, false);
    return 0;
}

SEC("fexit/xdp")
int BPF_PROG(fexit_xdp, struct xdp_buff *xdp, int verdict)
{
    handle_xdp(ctx, xdp, verdict, true);
    return 0;
}

对下面 XDP 程序进行 trace 的时候,

1
2
3
4
5
SEC("xdp")
int dummy(struct xdp_md *ctx)
{
    return XDP_PASS;
}

用户态的 Go 代码需要做的事情是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    xdpDummy := spec.Programs["dummy"]
    dummyProg, err := ebpf.NewProgram(xdpDummy)
    if err != nil {
        log.Fatalf("Failed to create dummy program: %v", err)
    }
    defer dummyProg.Close()

    xdpFentry := spec.Programs["fentry_xdp"]
    xdpFentry.AttachTarget = dummyProg
    xdpFentry.AttachTo = "dummy"
    xdpFexit := spec.Programs["fexit_xdp"]
    xdpFexit.AttachTarget = dummyProg
    xdpFexit.AttachTo = "dummy"
  1. 第一步,创建 XDP 程序。
  2. 第二步,给 fentryfexit 程序指定 AttachTargetAttachTo
  3. 其中,AttachTarget 是 XDP 程序,AttachTo 是 XDP 程序中的函数名。
  4. 即,将 fentryfexit 程序 attach 到 XDP 程序的 dummy 函数上。

P.S. demo 源代码:GitHub Asphaltt/learn-by-example/ebpf/fentry_fexit-xdp

fentry/fexit 的函数参数

仔细对比上面 fentry/fexit 的函数定义和 XDP 程序的函数定义:

1
2
3
4
5
6
7
8
SEC("fentry/xdp")
int BPF_PROG(fentry_xdp, struct xdp_buff *xdp);

SEC("fexit/xdp")
int BPF_PROG(fexit_xdp, struct xdp_buff *xdp, int verdict);

SEC("xdp")
int dummy(struct xdp_md *ctx);

为什么 fentry/fexit 的函数参数是 struct xdp_buff *xdp,而 XDP 程序的函数参数是 struct xdp_md *ctx 呢?

fentry/fexit 的函数参数里不能再使用 ctx

这是因为 BPF_PROG() 宏里已默认提供了 ctx 参数,所以不能再使用 ctx 参数名了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#define BPF_PROG(name, args...)                                     \
name(unsigned long long *ctx);                                      \
static __always_inline typeof(name(0))                              \
____##name(unsigned long long *ctx, ##args);                        \
typeof(name(0)) name(unsigned long long *ctx)                       \
{                                                                   \
    _Pragma("GCC diagnostic push")                                  \
    _Pragma("GCC diagnostic ignored \"-Wint-conversion\"")          \
    return ____##name(___bpf_ctx_cast(args));                       \
    _Pragma("GCC diagnostic pop")                                   \
}                                                                   \
static __always_inline typeof(name(0))                              \
____##name(unsigned long long *ctx, ##args)

fentry/fexit 的函数参数类型用的是 struct xdp_buff * 而不是 struct xdp_md *

在理解了 fentry/fexit 的实现原理后,就知道 fentry/fexit 程序接受的参数是目标函数真实的参数,而非 XDP 程序所使用的参数。

所以,fentry/fexit 程序的目标函数是 XDP 程序里的入口函数时,fentry/fexit 程序接受的参数是 struct xdp_buff *

 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
// ${KERNEL}/net/core/dev.c

u32 bpf_prog_run_generic_xdp(struct sk_buff *skb, struct xdp_buff *xdp,
                 struct bpf_prog *xdp_prog)
{
    // ...

    rxqueue = netif_get_rxqueue(skb);
    xdp_init_buff(xdp, frame_sz, &rxqueue->xdp_rxq);
    xdp_prepare_buff(xdp, hard_start, skb_headroom(skb) - mac_len,
             skb_headlen(skb) + mac_len, true);

    // ...

    act = bpf_prog_run_xdp(xdp_prog, xdp);

    // ...
}

// ${KERNEL}/include/linux/filter.h

static __always_inline u32 bpf_prog_run_xdp(const struct bpf_prog *prog,
                        struct xdp_buff *xdp)
{
    /* Driver XDP hooks are invoked within a single NAPI poll cycle and thus
     * under local_bh_disable(), which provides the needed RCU protection
     * for accessing map entries.
     */
    u32 act = __bpf_prog_run(prog, xdp, BPF_DISPATCHER_FUNC(xdp));

    if (static_branch_unlikely(&bpf_master_redirect_enabled_key)) {
        if (act == XDP_TX && netif_is_bond_slave(xdp->rxq->dev))
            act = xdp_master_redirect(xdp);
    }

    return act;
}

运行 XDP 程序时传的参数是 struct xdp_buff *xdp

XDP 程序的入参为什么是 struct xdp_md *

其实,这是一个约定,约定 XDP 程序的参数是 struct xdp_md *。而在运行的时候,从实参中读取对应的属性。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
struct xdp_md {
    __u32 data;
    __u32 data_end;
    __u32 data_meta;
    __u32 ingress_ifindex;
    __u32 rx_queue_index;
    __u32 egress_ifindex;
};

struct xdp_buff {
    void *data;
    void *data_end;
    void *data_meta;
    void *data_hard_start;
    struct xdp_rxq_info *rxq;
    struct xdp_txq_info *txq;
    u32 frame_sz;
    u32 flags;
};

那么,运行的时候是怎么从实参中读取对应的属性呢?

真实情况是,在 verifier 阶段,就会将 struct xdp_md * 的属性访问替换成对应的 struct xdp_buff * 的属性访问。

 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
59
60
61
62
63
64
65
// ${KERNEL}/net/core/filter.c

const struct bpf_verifier_ops xdp_verifier_ops = {
    .get_func_proto     = xdp_func_proto,
    .is_valid_access    = xdp_is_valid_access,
    .convert_ctx_access = xdp_convert_ctx_access,
    .gen_prologue       = bpf_noop_prologue,
    .btf_struct_access  = xdp_btf_struct_access,
};

static u32 xdp_convert_ctx_access(enum bpf_access_type type,
                  const struct bpf_insn *si,
                  struct bpf_insn *insn_buf,
                  struct bpf_prog *prog, u32 *target_size)
{
    struct bpf_insn *insn = insn_buf;

    switch (si->off) {
    case offsetof(struct xdp_md, data):
        *insn++ = BPF_LDX_MEM(BPF_FIELD_SIZEOF(struct xdp_buff, data),
                      si->dst_reg, si->src_reg,
                      offsetof(struct xdp_buff, data));
        break;
    case offsetof(struct xdp_md, data_meta):
        *insn++ = BPF_LDX_MEM(BPF_FIELD_SIZEOF(struct xdp_buff, data_meta),
                      si->dst_reg, si->src_reg,
                      offsetof(struct xdp_buff, data_meta));
        break;
    case offsetof(struct xdp_md, data_end):
        *insn++ = BPF_LDX_MEM(BPF_FIELD_SIZEOF(struct xdp_buff, data_end),
                      si->dst_reg, si->src_reg,
                      offsetof(struct xdp_buff, data_end));
        break;
    case offsetof(struct xdp_md, ingress_ifindex):
        *insn++ = BPF_LDX_MEM(BPF_FIELD_SIZEOF(struct xdp_buff, rxq),
                      si->dst_reg, si->src_reg,
                      offsetof(struct xdp_buff, rxq));
        *insn++ = BPF_LDX_MEM(BPF_FIELD_SIZEOF(struct xdp_rxq_info, dev),
                      si->dst_reg, si->dst_reg,
                      offsetof(struct xdp_rxq_info, dev));
        *insn++ = BPF_LDX_MEM(BPF_W, si->dst_reg, si->dst_reg,
                      offsetof(struct net_device, ifindex));
        break;
    case offsetof(struct xdp_md, rx_queue_index):
        *insn++ = BPF_LDX_MEM(BPF_FIELD_SIZEOF(struct xdp_buff, rxq),
                      si->dst_reg, si->src_reg,
                      offsetof(struct xdp_buff, rxq));
        *insn++ = BPF_LDX_MEM(BPF_W, si->dst_reg, si->dst_reg,
                      offsetof(struct xdp_rxq_info,
                           queue_index));
        break;
    case offsetof(struct xdp_md, egress_ifindex):
        *insn++ = BPF_LDX_MEM(BPF_FIELD_SIZEOF(struct xdp_buff, txq),
                      si->dst_reg, si->src_reg,
                      offsetof(struct xdp_buff, txq));
        *insn++ = BPF_LDX_MEM(BPF_FIELD_SIZEOF(struct xdp_txq_info, dev),
                      si->dst_reg, si->dst_reg,
                      offsetof(struct xdp_txq_info, dev));
        *insn++ = BPF_LDX_MEM(BPF_W, si->dst_reg, si->dst_reg,
                      offsetof(struct net_device, ifindex));
        break;
    }

    return insn - insn_buf;
}

小结

  • fentry/fexit 的函数参数是目标函数真实的参数,而非 XDP 程序所使用的参数。
  • fexit 能够获取到目标函数的返回值,即 XDP 程序的返回值。
  • XDP 程序的入参为什么是 struct xdp_md *?这是一个约定,约定 XDP 程序的参数是 struct xdp_md *
  • 在 verifier 阶段,就会将 struct xdp_md * 的属性访问替换成对应的 struct xdp_buff * 的属性访问。

看到 xdp_verifier_ops,不枉我对 bpf verifier 的一番探索。