#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);
}
数据面程序:https://github.com/torvalds/linux/blob/v5.10/tools/testing/selftests/bpf/progs/tailcall2.c
控制面程序:https://github.com/torvalds/linux/blob/v5.10/tools/testing/selftests/bpf/prog_tests/tailcalls.c#L147
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 上的下一个子程序。
https://github.com/github/glb-director/pull/96#issue-379348950
https://github.com/github/glb-director/blob/v1.0.7/src/glb-director-xdp/bpf/tailcall.c
这个比较难用,也有改进的方案,但短期内可能还得继续使用这个难用的方法。
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 程序:https://pkg.go.dev/github.com/cilium/ebpf
提供 ip 命令相关功能的 API 接口:https://pkg.go.dev/github.com/vishvananda/netlink
加载 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_MAPS
和 BPF_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。