#0108 xdp 进阶

bpf_tail_call

bpf_tail_call 需要配合 BPF_MAP_TYPE_PROG_ARRAY 类型的 Map 使用。

首先,在数据面程序中:

  • 定义一个 BPF_MAP_TYPE_PROG_ARRAY 类型的 Map。

  • 定义要 bpf_tail_call 的子程序。

  • 在入口程序里使用 bpf_tail_call 通过引用 Map 中的索引的方式调用子程序。

// BPF_MAP_TYPE_PROG_ARRAY 类型的 Map
struct {
    __uint(type, BPF_MAP_TYPE_PROG_ARRAY);
    __uint(max_entries, 3);
    __uint(key_size, sizeof(__u32));
    __uint(value_size, sizeof(__u32));
} jmp_table SEC(".maps");

// bpf_tail_call 要调用的子程序
SEC("classifier/0")
int bpf_func_0(struct __sk_buff *skb)
{
    bpf_tail_call_static(skb, &jmp_table, 1);
    return 0;
}

SEC("classifier/1")
int bpf_func_1(struct __sk_buff *skb)
{
    bpf_tail_call_static(skb, &jmp_table, 2);
    return 1;
}

// 入口程序
SEC("classifier")
int entry(struct __sk_buff *skb)
{
    bpf_tail_call_static(skb, &jmp_table, 0);
    return 3;
}

然后在控制面程序中,加载数据面程序,从其中获取要调用的子程序的 fd 并将其填入 jmp_table 中。

prog_array = bpf_object__find_map_by_name(obj, "jmp_table");
map_fd = bpf_map__fd(prog_array);
for (i = 0; i < bpf_map__def(prog_array)->max_entries; i++) {
    snprintf(prog_name, sizeof(prog_name), "classifier/%i", i);
    prog = bpf_object__find_program_by_title(obj, prog_name);
    prog_fd = bpf_program__fd(prog);
    err = bpf_map_update_elem(map_fd, &i, &prog_fd, BPF_ANY);
}

chain call

使用 bpf_tail_call 可以实现 chain call,方法就是定义一个 prog array,将具体的子程序的 fd 写入这个数组中,定义一个 root xdp 程序,这个程序挂载到网卡接口,这个程序遍历 prog array 并调用遇到的第一个合法程序。

struct bpf_map_def SEC("maps") root_array = {
    .type = BPF_MAP_TYPE_PROG_ARRAY,
    .key_size = sizeof(__u32),
    .value_size = sizeof(__u32),
    .max_entries = ROOT_ARRAY_SIZE,
};

SEC("xdp-root")
int xdp_root(struct xdp_md *ctx) {
    #pragma clang loop unroll(full)
    for (__u32 i = 0; i < ROOT_ARRAY_SIZE; i++) {
        bpf_tail_call(ctx, &root_array, i);
    }
    return XDP_PASS;
}

注意 bpf_tail_call 调用如果成功是不会返回的,所以每个子程序结束的时候仍需通过 bpf_tail_call 手工调用 chain 上的下一个子程序。

这个比较难用,也有改进的方案,但短期内可能还得继续使用这个难用的方法。

xdpcap

bpf_tail_call 还可以实现动态 hook,典型的应用就是 xdpcap。xdpcap 的使用方法如下:

#include "hook.h"

struct bpf_map_def xdpcap_hook = XDPCAP_HOOK();

int xdp_prog(struct xdp_md *ctx) {
    // ...
    return xdpcap_exit(ctx, &xdpcap_hook, XDP_PASS);
}

控制面程序需要将 xdpcap_hook 这个 map pin 到 bpf fs 中,然后就可以通过这个 pinned map 抓包了。

$ xdpcap /path/to/pinned/map dump.pcap "tcp and port 80"

从 xdpcap 的 hook.h 我们可以看到 xdpcap_hook 的定义是一个 prog array,xdpcap_exit 是一个调用 bpf_tail_call 的函数。

// hook.h
#define XDPCAP_HOOK() { \
    .type = BPF_MAP_TYPE_PROG_ARRAY, \
    .key_size = sizeof(int), \
    .value_size = sizeof(int), \
    .max_entries = 5, \
}

static inline enum xdp_action xdpcap_exit(struct xdp_md *ctx, void *hook_map, enum xdp_action action) {
    bpf_tail_call(ctx, hook_map, action);
    return action;
}

xdpcap 将抓包的程序的 fd 写入到 xdpcap_hook prog array 中即可开始抓包,删除就停止。

使用 go ebpf 库

主要使用以下两个库:

加载 ebpf 程序:

package main

import (
    log "github.com/sirupsen/logrus"
    "github.com/cilium/ebpf"
    "github.com/vishvananda/netlink"
)

func main() {
    coll, err := ebpf.LoadCollection("xdp-prog.o")
    if err != nil {
        log.Fatal(err)
    }
    defer coll.Close()

    prog := coll.Programs["xdp-pass"]
    if prog == nil {
        log.Fatal("prog not found")
    }

    link, err := netlink.LinkByName("lo")
    if err != nil {
        log.Fatal(err)
    }
    // 如果 fd = -1,那么就是卸载 xdp
    err = netlink.LinkSetXdpFd(link, prog.FD())
    if err != nil {
        log.Fatal(err)
    }
}

Map In Map

BPF_MAP_TYPE_ARRAY_OF_MAPSBPF_MAP_TYPE_HASH_OF_MAPS 是两种比较特殊的 Map,这两个 Map 中的值存储的不是不同的普通的数据结构,而是另外一个 Map。

一个例子,glb 中的转发表就是使用 map in map 存储的:

// VIP 地址 -> 转发表数组索引的映射
struct bpf_map_def SEC("maps") glb_binds = {
    .type = BPF_MAP_TYPE_HASH,
    .key_size = sizeof(struct glb_bind),
    .value_size = sizeof(uint32_t),
    .max_entries = 4096,
};

// 转发表数组,类型为 map in map,所以不用定义 value size,因为 value 为 inner map
// 的 fd,大小固定为 32。
struct bpf_map_def SEC("maps") glb_tables = {
    .type = BPF_MAP_TYPE_ARRAY_OF_MAPS,
    .key_size = sizeof(uint32_t),
    .max_entries = 4096,
};

查找:

// 根据 VIP 找到转发表的索引
uint32_t *table_id_ptr = (uint32_t *)bpf_map_lookup_elem(&glb_binds, &bind);
uint32_t table_id = *table_id_ptr;
// 根据转发表索引找到转发表的 fd
struct bpf_map_def *table = (struct bpf_map_def *)bpf_map_lookup_elem(&glb_tables, &table_id);
// 查找转发表
uint32_t *tableRow = (uint32_t *)bpf_map_lookup_elem(table, &tableRowIndex);

生成:

tableSpec := &ebpf.MapSpec{
    Type:       ebpf.Array,
    KeySize:    4,
    ValueSize:  8,
    MaxEntries: 0x10000,
}
table, err := ebpf.NewMap(tableSpec)
// 填充转发表 ...

tableIndex := uint32(0)
tableFd := table.FD()
tableArray := app.Collection.Maps["glb_tables"]
if err := tableArray.Put(unsafe.Pointer(&tableIndex), unsafe.Pointer(&tableFd)); err != nil {
    log.Fatal(err)
}

外层 map 中记录的实际是内层 map 的 id。

# bpftool map
1122: array_of_maps  name glb_tables  flags 0x0
    key 4B  value 4B  max_entries 4096  memlock 36864B
1123: hash  name glb_binds  flags 0x0
    key 24B  value 4B  max_entries 4096  memlock 397312B
1125: array  flags 0x0
    key 4B  value 8B  max_entries 65536  memlock 528384B
$ bpftool map dump id 1122
key: 00 00 00 00  value: 65 04 00 00

如上, dump 出的 value 0x0465 = 1125,即是 inner map 的 id。