feat: add tcx and bpf token tutorials (#203)

* feat: add tcx and bpf token tutorials

* docs: auto-generate documentation

* docs: rewrite tcx and bpf_token tutorials with richer content and consistent style

Rewrote all 4 README files (EN/ZH for both tutorials) to match the
existing tutorial style with detailed background, full code listings,
step-by-step explanations, comparison tables, and proper references.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style: replace em dashes with colons, commas, and parentheses

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: auto-generate documentation

* ci: add tcx and bpf_token builds to CI; simplify execl in token_userns_demo

- Add make targets for src/50-tcx and src/features/bpf_token to the
  test-libbpf CI workflow.
- Replace four separate execl() calls with a single execv() using a
  dynamically built argv array, reducing complexity and eliminating
  CodeFactor command-injection false positives.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci: fix mkdocs path in trigger-sync workflow

Use .venv/bin/mkdocs instead of bare mkdocs, since make install
puts it inside a virtualenv.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: LinuxDev9002 <linuxdev8883@example.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
云微
2026-03-10 21:23:23 -07:00
committed by GitHub
parent ee6d522e40
commit 3a722c03d5
22 changed files with 2212 additions and 1 deletions

View File

@@ -0,0 +1,2 @@
level=Depth
type=Features

13
src/features/bpf_token/.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
# Build artifacts
.output/
*.o
*.skel.h
# Generated binaries
token_trace
token_userns_demo
# Editor files
*.swp
*~
.vscode/

View File

@@ -0,0 +1,93 @@
# SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
OUTPUT := .output
CLANG ?= clang
LIBBPF_SRC := $(abspath ../../third_party/libbpf/src)
BPFTOOL_SRC := $(abspath ../../third_party/bpftool/src)
LIBBPF_OBJ := $(abspath $(OUTPUT)/libbpf.a)
BPFTOOL_OUTPUT ?= $(abspath $(OUTPUT)/bpftool)
BPFTOOL ?= $(BPFTOOL_OUTPUT)/bootstrap/bpftool
ARCH ?= $(shell uname -m | sed 's/x86_64/x86/' \
| sed 's/arm.*/arm/' \
| sed 's/aarch64/arm64/' \
| sed 's/ppc64le/powerpc/' \
| sed 's/mips.*/mips/' \
| sed 's/riscv64/riscv/' \
| sed 's/loongarch64/loongarch/')
VMLINUX := ../../third_party/vmlinux/$(ARCH)/vmlinux.h
INCLUDES := -I$(OUTPUT) -I../../third_party/libbpf/include/uapi -I$(dir $(VMLINUX)) -I.
CFLAGS := -g -Wall
ALL_LDFLAGS := $(LDFLAGS) $(EXTRA_LDFLAGS)
BPF_APPS = token_trace
APPS = $(BPF_APPS) token_userns_demo
CLANG_BPF_SYS_INCLUDES ?= $(shell $(CLANG) -v -E - </dev/null 2>&1 \
| sed -n '/<...> search starts here:/,/End of search list./{ s| \(/.*\)|-idirafter \1|p }')
ifeq ($(V),1)
Q =
msg =
else
Q = @
msg = @printf ' %-8s %s%s\n' \
"$(1)" \
"$(patsubst $(abspath $(OUTPUT))/%,%,$(2))" \
"$(if $(3), $(3))";
MAKEFLAGS += --no-print-directory
endif
define allow-override
$(if $(or $(findstring environment,$(origin $(1))),\
$(findstring command line,$(origin $(1)))),,\
$(eval $(1) = $(2)))
endef
$(call allow-override,CC,$(CROSS_COMPILE)cc)
$(call allow-override,LD,$(CROSS_COMPILE)ld)
.PHONY: all
all: $(APPS)
.PHONY: clean
clean:
$(call msg,CLEAN)
$(Q)rm -rf $(OUTPUT) $(APPS)
$(OUTPUT) $(OUTPUT)/libbpf $(BPFTOOL_OUTPUT):
$(call msg,MKDIR,$@)
$(Q)mkdir -p $@
$(LIBBPF_OBJ): $(wildcard $(LIBBPF_SRC)/*.[ch] $(LIBBPF_SRC)/Makefile) | $(OUTPUT)/libbpf
$(call msg,LIB,$@)
$(Q)$(MAKE) -C $(LIBBPF_SRC) BUILD_STATIC_ONLY=1 \
OBJDIR=$(dir $@)/libbpf DESTDIR=$(dir $@) \
INCLUDEDIR= LIBDIR= UAPIDIR= \
install
$(BPFTOOL): | $(BPFTOOL_OUTPUT)
$(call msg,BPFTOOL,$@)
$(Q)$(MAKE) ARCH= CROSS_COMPILE= OUTPUT=$(BPFTOOL_OUTPUT)/ -C $(BPFTOOL_SRC) bootstrap
$(OUTPUT)/%.bpf.o: %.bpf.c $(LIBBPF_OBJ) $(wildcard %.h) $(VMLINUX) | $(OUTPUT) $(BPFTOOL)
$(call msg,BPF,$@)
$(Q)$(CLANG) -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) \
$(INCLUDES) $(CLANG_BPF_SYS_INCLUDES) \
-c $(filter %.c,$^) -o $(patsubst %.bpf.o,%.tmp.bpf.o,$@)
$(Q)$(BPFTOOL) gen object $@ $(patsubst %.bpf.o,%.tmp.bpf.o,$@)
$(OUTPUT)/%.skel.h: $(OUTPUT)/%.bpf.o | $(OUTPUT) $(BPFTOOL)
$(call msg,GEN-SKEL,$@)
$(Q)$(BPFTOOL) gen skeleton $< > $@
$(patsubst %,$(OUTPUT)/%.o,$(BPF_APPS)): %.o: %.skel.h
$(OUTPUT)/%.o: %.c $(wildcard %.h) | $(OUTPUT)
$(call msg,CC,$@)
$(Q)$(CC) $(CFLAGS) $(INCLUDES) -c $(filter %.c,$^) -o $@
$(APPS): %: $(OUTPUT)/%.o $(LIBBPF_OBJ) | $(OUTPUT)
$(call msg,BINARY,$@)
$(Q)$(CC) $(CFLAGS) $^ $(ALL_LDFLAGS) -lelf -lz -o $@
.DELETE_ON_ERROR:
.SECONDARY:

View File

@@ -0,0 +1,289 @@
# eBPF Tutorial by Example: BPF Token for Delegated Privilege and Secure Program Loading
Ever needed to let a container or CI job load an eBPF program without giving it full `CAP_BPF` or `CAP_SYS_ADMIN`? Or wanted to expose XDP packet processing to a tenant workload while ensuring it can only create the specific map types and program types you've approved? Before BPF token, the answer was binary: either you had the capabilities to do *everything* in BPF, or you could do *nothing*. There was no middle ground.
This is what **BPF Token** solves. Introduced by Andrii Nakryiko and merged in Linux 6.9, BPF token is a delegation mechanism that lets a privileged process (like a container runtime or systemd) create a precisely scoped permission set for BPF operations, then hand it to an unprivileged process through a bpffs mount. The unprivileged process can load programs, create maps, and attach hooks, but only the types that were explicitly allowed. No broad capabilities required.
In this tutorial, we'll set up a delegated bpffs mount in a user namespace, derive a BPF token from it, and use libbpf to load and attach a minimal XDP program, all from a process that has zero BPF capabilities of its own.
> The complete source code: <https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/features/bpf_token>
## Introduction to BPF Token: Solving the Privilege Problem
### The Problem: All-or-Nothing BPF Capabilities
Traditional eBPF requires `CAP_BPF` for program loading and map creation, plus additional capabilities like `CAP_PERFMON` for tracing, `CAP_NET_ADMIN` for networking hooks, and `CAP_SYS_ADMIN` for certain advanced operations. These capabilities are inherently **system-wide**: you cannot namespace or sandbox `CAP_BPF`. As the kernel documentation explains, this is by design: BPF tracing helpers like `bpf_probe_read_kernel()` can access arbitrary kernel memory, which fundamentally cannot be scoped to a single namespace.
This creates a real problem in multi-tenant environments:
1. **Container isolation**: A Kubernetes pod that needs to run a simple XDP program must be given `CAP_BPF` + `CAP_NET_ADMIN`, which also grants it the ability to load *any* BPF program type and create *any* map type. There's no way to say "you can load XDP programs but not kprobes."
2. **CI/CD pipelines**: A build job that tests an eBPF-based observability tool needs root-equivalent capabilities to load programs, even though the test only exercises a specific, well-known program type.
3. **Third-party integrations**: A service mesh sidecar that attaches sockops programs needs capabilities that also grant it the ability to trace every process on the host.
The result is that organizations either give broad BPF capabilities (weakening their security posture) or prohibit BPF entirely in unprivileged contexts (limiting the technology's adoption).
### The Solution: Scoped Delegation Through bpffs
BPF token takes a different approach. Instead of trying to namespace capabilities (which is fundamentally unsafe for BPF), it introduces an explicit delegation model:
1. A **privileged process** (container runtime, init system, platform daemon) creates a bpffs instance with specific delegation options that define exactly which BPF operations are allowed.
2. The privileged process passes this bpffs mount to an **unprivileged process** (container, CI job, tenant workload).
3. The unprivileged process derives a **BPF token** from the bpffs mount. The token is a file descriptor that carries the delegated permission set.
4. When the unprivileged process makes `bpf()` syscalls (through libbpf or directly), it passes the token fd. The kernel checks permissions against the token instead of against the process's capabilities.
The token is scoped along four independent axes:
| Delegation Option | What It Controls | Example |
|-------------------|-----------------|---------|
| `delegate_cmds` | Which `bpf()` commands are allowed | `prog_load:map_create:btf_load:link_create` |
| `delegate_maps` | Which map types can be created | `array:hash:ringbuf` |
| `delegate_progs` | Which program types can be loaded | `xdp:socket_filter` |
| `delegate_attachs` | Which attach types are allowed | `xdp:cgroup_inet_ingress` or `any` |
Each axis is a bitmask. If a bit isn't set, the corresponding operation is denied even if the token is present. This gives platform engineers fine-grained control: you can allow a container to load XDP programs with array maps but deny it access to kprobes, perf events, or hash-of-maps.
### The User Namespace Constraint
One critical design decision: **a BPF token must be created inside the same user namespace as the bpffs instance, and that user namespace must not be `init_user_ns`**. This is intentional. It means:
- A host-namespace bpffs (the one at `/sys/fs/bpf`) does **not** produce usable tokens. Tokens only work when the bpffs is associated with a non-init user namespace.
- The privileged parent configures the bpffs before passing it to the child, but the child (in its own user namespace) is the one that creates and uses the token.
- This design prevents a process with an existing token from using it to escalate privileges outside its namespace boundary.
### How libbpf Makes It Transparent
For applications built with libbpf (which is most of them), token usage is nearly transparent. You have three options:
1. **Explicit path**: Set `bpf_object_open_opts.bpf_token_path` when opening the BPF object. libbpf will derive the token from the specified bpffs mount.
2. **Environment variable**: Set `LIBBPF_BPF_TOKEN_PATH` to point to the bpffs mount. libbpf picks it up automatically.
3. **Default path**: If the default `/sys/fs/bpf` is a delegated bpffs in the current user namespace, libbpf uses it implicitly.
Once the token is derived, libbpf passes it to every relevant syscall (`BPF_MAP_CREATE`, `BPF_BTF_LOAD`, `BPF_PROG_LOAD`, and `BPF_LINK_CREATE`) without any source-code changes in the BPF application.
## Writing the eBPF Program
The BPF side of this demo is intentionally minimal: a tiny XDP program on loopback. This keeps the focus on the token workflow. Here's the complete source:
```c
// SPDX-License-Identifier: GPL-2.0
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
char LICENSE[] SEC("license") = "GPL";
struct token_stats {
__u64 packets;
__u32 last_ifindex;
};
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 1);
__type(key, __u32);
__type(value, struct token_stats);
} stats_map SEC(".maps");
SEC("xdp")
int handle_packet(struct xdp_md *ctx)
{
struct token_stats *stats;
__u32 key = 0;
stats = bpf_map_lookup_elem(&stats_map, &key);
if (!stats)
return 0;
stats->packets++;
stats->last_ifindex = ctx->ingress_ifindex;
return XDP_PASS;
}
```
A few design choices to note:
**`BPF_MAP_TYPE_ARRAY`** was chosen because the delegation policy explicitly allows `array` maps. If we had used a hash map instead, loading would fail because the token doesn't grant `hash` map creation permission. This is the token model in action; even trivial program changes can be caught by the delegation policy.
**`SEC("xdp")`** matches the `delegate_progs=xdp` policy. If you changed this to `SEC("kprobe/...")`, the kernel would reject it at load time with an `EPERM` because kprobe isn't in the allowed program types.
**`XDP_PASS`** simply lets every packet through. The program's only purpose is to prove that a token-backed load and attach succeeded. In production, you'd replace this with real packet-processing logic.
## User-Space Loader: Token-Backed Loading
The `token_trace.c` loader is a standard libbpf skeleton program with one key addition: it passes a `bpf_token_path`:
```c
struct bpf_object_open_opts open_opts = {};
open_opts.sz = sizeof(open_opts);
open_opts.bpf_token_path = env.token_path;
skel = token_trace_bpf__open_opts(&open_opts);
```
From this point on, libbpf takes over. When it calls `bpf(BPF_MAP_CREATE)` to create `stats_map`, it includes the token fd. When it calls `bpf(BPF_PROG_LOAD)` for the XDP program, it includes the token fd. When it calls `bpf(BPF_LINK_CREATE)` to attach to the interface, it includes the token fd.
The rest of the loader is straightforward:
```c
err = token_trace_bpf__load(skel); // token used for map_create + prog_load
link = bpf_program__attach_xdp(skel->progs.handle_packet, ifindex); // token used for link_create
```
After attaching, the loader reads the map before and after generating a test packet to verify the program executed:
```c
err = bpf_map_lookup_elem(map_fd, &key, &before);
// ... generate UDP packet to 127.0.0.1 ...
err = bpf_map_lookup_elem(map_fd, &key, &after);
printf("delta : %llu\n", after.packets - before.packets);
```
If the delta is 1, the XDP program was successfully loaded and attached using only delegated capabilities.
## The Namespace Orchestrator: `token_userns_demo`
Because BPF token requires a non-init user namespace, running a bare `token_trace -t /sys/fs/bpf` on the host won't work. The `token_userns_demo.c` wrapper automates the complex namespace choreography. Here's the full sequence:
### Step 1: Fork and Create Namespaces
```
parent (root, init_user_ns) child (unprivileged, new userns)
│ │
│ fork() │
├────────────────────────────────────────>│
│ │
│ unshare(CLONE_NEWUSER)
│ unshare(CLONE_NEWNS | CLONE_NEWNET)
```
The child creates a new user namespace (where it maps itself to uid/gid 0), a new mount namespace (so bpffs mounts are private), and a new network namespace (so `lo` is a fresh interface it can attach to).
### Step 2: Create bpffs and Configure Delegation
```
parent (root, init_user_ns) child (new userns)
│ │
│ fs_fd = fsopen("bpf", 0)
│ <───── send fs_fd via SCM_RIGHTS ────│
│ │
fsconfig(fs_fd, "delegate_cmds", ...) │ (waiting for ack)
fsconfig(fs_fd, "delegate_maps", "array") │
fsconfig(fs_fd, "delegate_progs", "xdp:...") │
fsconfig(fs_fd, "delegate_attachs", "any") │
fsconfig(fs_fd, FSCONFIG_CMD_CREATE) │
│ │
│ ───────── send ack ─────────────────>│
```
The child calls `fsopen("bpf", 0)` to create a bpffs filesystem context in its user namespace, then sends the file descriptor to the parent via a Unix socket (`SCM_RIGHTS`). The parent, running as root in the init namespace, configures the delegation policy with `fsconfig()`, then materializes the filesystem with `FSCONFIG_CMD_CREATE`.
This two-step dance is necessary because: (a) the bpffs must be created in the child's user namespace (for the token to be valid there), but (b) only the privileged parent can set delegation options (because those options grant BPF capabilities).
### Step 3: Mount and Load
```
child (new userns)
│
mnt_fd = fsmount(fs_fd, 0, 0)
token_path = "/proc/self/fd/<mnt_fd>"
set_loopback_up()
exec("./token_trace", "-t", token_path, "-i", "lo")
```
The child materializes the bpffs as a detached mount (no mount point needed, since `/proc/self/fd/<mnt_fd>` gives a path), brings the loopback interface up in its network namespace, and `exec`s `token_trace` with the bpffs path. From `token_trace`'s perspective, it's just opening a BPF object with a token path. It doesn't know or care about the namespace setup.
## Preparing a bpffs Mount Manually
If you want to experiment with the mount syntax outside the demo wrapper, the repository includes a helper script:
```bash
cd bpf-developer-tutorial/src/features/bpf_token
bash setup_token_bpffs.sh /tmp/bpf-token
```
This mounts bpffs at `/tmp/bpf-token` with:
```text
delegate_cmds=prog_load:map_create:btf_load:link_create
delegate_maps=array
delegate_progs=xdp:socket_filter
delegate_attachs=any
```
**Why `socket_filter`?** libbpf performs a trivial program-load probe before loading the real BPF object. This probe uses a generic `BPF_PROG_TYPE_SOCKET_FILTER` program to detect kernel feature support. Without `socket_filter` in the delegation policy, the probe fails and libbpf refuses to proceed.
**Why `delegate_attachs=any`?** The same libbpf probe path also triggers attach-type validation in the kernel's token checking code. Using `any` avoids having to enumerate every possible attach type for probe compatibility.
Note that a host-namespace mount like this is useful for inspecting the delegation policy (e.g., with `bpftool token list`), but won't produce working tokens unless the `bpf(BPF_TOKEN_CREATE)` syscall comes from a matching non-init user namespace.
## Compilation and Execution
Build all binaries:
```bash
cd bpf-developer-tutorial/src/features/bpf_token
make
```
Run the end-to-end demo:
```bash
sudo ./token_userns_demo
```
Expected output:
```text
token path : /proc/self/fd/5
interface : lo (ifindex=1)
packets before : 0
packets after : 1
delta : 1
last ifindex : 1
```
The `delta: 1` confirms that the XDP program was successfully loaded and attached using a BPF token, with no `CAP_BPF` or `CAP_SYS_ADMIN` in the child process.
Add `-v` for verbose libbpf output to see the token being created and used:
```bash
sudo ./token_userns_demo -v
```
If you already manage your own delegated bpffs in a user namespace, you can run the loader directly:
```bash
./token_trace -t /proc/self/fd/<mnt-fd> -i lo
```
## Real-World Applications
While this tutorial uses a minimal XDP program, the BPF token pattern scales to production scenarios:
- **Container runtimes** (LXD, Docker, Kubernetes): Mount a delegated bpffs into a container with only the program and map types the workload needs. LXD already supports this through its `security.delegate_bpf` option.
- **CI/CD testing**: Give build jobs the ability to load and test specific eBPF programs without granting them host-level capabilities. The delegation policy acts as an allowlist for BPF operations.
- **Multi-tenant BPF platforms**: A platform daemon creates per-tenant bpffs mounts with different delegation policies. One tenant might be allowed XDP + array maps, while another might get tracepoint + ringbuf access.
- **LSM integration**: Because BPF tokens integrate with Linux Security Modules, you can combine token delegation with SELinux or AppArmor policies for defense-in-depth. Each token gets its own security context that LSM hooks can inspect.
## Summary
In this tutorial, we learned how BPF token provides a delegation model for eBPF privilege that goes beyond the binary "all or nothing" of Linux capabilities. We walked through the complete flow: a privileged parent configures a bpffs instance with specific delegation options, an unprivileged child in a user namespace derives a token from that bpffs, and libbpf transparently uses the token for map creation, program loading, and attachment. The result is a minimal XDP program running in an unprivileged context, something that was impossible before Linux 6.9.
BPF token is not a niche feature. It represents the kernel's answer to a fundamental question in the eBPF ecosystem: how do you safely share BPF capabilities in a multi-tenant world without granting unconstrained access to the BPF subsystem?
If you'd like to learn more about eBPF, visit our tutorial code repository at <https://github.com/eunomia-bpf/bpf-developer-tutorial> or website <https://eunomia.dev/tutorials/> for more examples and complete tutorials.
## References
- [BPF Token concept documentation](https://docs.ebpf.io/linux/concepts/token/)
- [BPF token kernel patch series (Andrii Nakryiko)](https://lore.kernel.org/bpf/20240103222034.2582628-1-andrii@kernel.org/T/)
- [BPF token LWN article](https://lwn.net/Articles/959350/)
- [Finer-grained BPF tokens LWN discussion](https://lwn.net/Articles/947173/)
- [Privilege delegation using BPF Token (LXD documentation)](https://documentation.ubuntu.com/lxd/latest/explanation/bpf/)
- [bpf_token_create() libbpf API](https://docs.ebpf.io/ebpf-library/libbpf/userspace/bpf_token_create/)
- <https://docs.kernel.org/bpf/>

View File

@@ -0,0 +1,289 @@
# eBPF 入门实践教程:BPF Token,安全的委托式权限与程序加载
你是否需要让容器或 CI 任务加载一个 eBPF 程序,但又不想给它完整的 `CAP_BPF` 或 `CAP_SYS_ADMIN`?或者你想把 XDP 数据包处理能力开放给租户工作负载,同时确保它只能创建你批准过的 map 类型和 program 类型?在 BPF token 出现之前,答案是二元的:要么你有能力在 BPF 中做*一切*,要么你*什么都做不了*。没有中间地带。
这就是 **BPF Token** 要解决的问题。BPF token 由 Andrii Nakryiko 开发,于 Linux 6.9 合入内核,它是一种委托机制,让特权进程(如容器运行时或 systemd)创建一组精确限定范围的 BPF 操作许可集合,然后通过 bpffs 挂载传递给非特权进程。非特权进程可以加载程序、创建 map、挂载 hook,但只能使用被显式允许的类型。不需要任何宽泛的 capability。
本教程将在 user namespace 中设置一个带委托策略的 bpffs 挂载,从中派生 BPF token,然后用 libbpf 加载并挂载一个最小的 XDP 程序。所有操作来自一个本身没有任何 BPF capability 的进程。
> 完整源代码: <https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/features/bpf_token>
## 背景:解决 BPF 权限问题
### 问题:全有或全无的 BPF Capability
传统 eBPF 需要 `CAP_BPF` 来加载程序和创建 map,还需要 `CAP_PERFMON`(用于 tracing)、`CAP_NET_ADMIN`(用于网络 hook)、`CAP_SYS_ADMIN`(用于某些高级操作)等额外的 capability。这些 capability 本质上是**系统级**的,你无法对 `CAP_BPF` 做 namespace 隔离或沙箱化。内核文档解释了原因:BPF tracing helper(如 `bpf_probe_read_kernel()`)可以访问任意内核内存,这在根本上无法被限定到单个 namespace 中。
这在多租户环境中造成了实际问题:
1. **容器隔离**:一个只需要运行简单 XDP 程序的 Kubernetes Pod 必须被赋予 `CAP_BPF` + `CAP_NET_ADMIN`,但这也同时赋予了它加载*任意* BPF 程序类型和创建*任意* map 类型的能力。你没办法说"你可以加载 XDP 程序但不能加载 kprobe"。
2. **CI/CD 流水线**:一个测试 eBPF 可观测工具的构建任务需要 root 级别的 capability 来加载程序,即使测试只涉及一个特定的、已知的程序类型。
3. **第三方集成**:一个 service mesh sidecar 需要挂载 sockops 程序的 capability,但这些 capability 同时也赋予了它 trace 主机上每个进程的能力。
结果就是:组织要么给出宽泛的 BPF capability(削弱安全态势),要么在非特权环境中完全禁止 BPF(限制了该技术的采用)。
### 解决方案:通过 bpffs 进行精确委托
BPF token 采取了不同的思路。它没有尝试对 capability 做 namespace 化(对 BPF 来说这根本不安全),而是引入了显式的委托模型:
1. **特权进程**(容器运行时、init 系统、平台守护进程)创建一个带有特定委托选项的 bpffs 实例,精确定义允许哪些 BPF 操作。
2. 特权进程将这个 bpffs 挂载传递给**非特权进程**(容器、CI 任务、租户工作负载)。
3. 非特权进程从 bpffs 挂载中派生**BPF token**。token 是一个文件描述符,承载着委托的权限集合。
4. 当非特权进程发起 `bpf()` 系统调用时(通过 libbpf 或直接调用),传入 token fd。内核根据 token 而不是进程的 capability 来检查权限。
token 沿四个独立轴进行限定:
| 委托选项 | 控制内容 | 示例 |
|----------|---------|------|
| `delegate_cmds` | 允许哪些 `bpf()` 命令 | `prog_load:map_create:btf_load:link_create` |
| `delegate_maps` | 允许创建哪些 map 类型 | `array:hash:ringbuf` |
| `delegate_progs` | 允许加载哪些程序类型 | `xdp:socket_filter` |
| `delegate_attachs` | 允许哪些 attach 类型 | `xdp:cgroup_inet_ingress` 或 `any` |
每个轴是一个位掩码。如果某个位未设置,对应的操作即使有 token 也会被拒绝。这给了平台工程师细粒度的控制:你可以允许容器加载带 array map 的 XDP 程序,但拒绝它访问 kprobe、perf event 或 hash-of-maps。
### User Namespace 约束
一个关键的设计决定:**BPF token 必须在和 bpffs 实例相同的 user namespace 中创建,且该 user namespace 不能是 `init_user_ns`**。这是有意为之。这意味着:
- 主机 namespace 下的 bpffs(`/sys/fs/bpf`)**不能**产生可用的 token。token 只在 bpffs 关联到非 init 的 user namespace 时才能工作。
- 特权父进程在将 bpffs 传给子进程之前配置好委托策略,但子进程(在自己的 user namespace 中)才是创建和使用 token 的一方。
- 这个设计防止持有 token 的进程利用它在 namespace 边界之外提升权限。
### libbpf 如何让它变得透明
对于基于 libbpf 构建的应用(大多数 eBPF 应用都是),token 的使用几乎是透明的。你有三种选择:
1. **显式路径**:在打开 BPF 对象时设置 `bpf_object_open_opts.bpf_token_path`。libbpf 会从指定的 bpffs 挂载中派生 token。
2. **环境变量**:设置 `LIBBPF_BPF_TOKEN_PATH` 指向 bpffs 挂载。libbpf 自动识别。
3. **默认路径**:如果默认的 `/sys/fs/bpf` 是当前 user namespace 中的委托 bpffs,libbpf 隐式使用它。
一旦 token 被派生,libbpf 会在每个相关的 syscall(`BPF_MAP_CREATE`、`BPF_BTF_LOAD`、`BPF_PROG_LOAD` 和 `BPF_LINK_CREATE`)中传递它,不需要修改 BPF 应用的任何源代码。
## 编写 eBPF 程序
本教程的 BPF 侧故意保持最小,只有 loopback 上的一个 XDP 小程序。这样可以把注意力集中在 token 工作流上。以下是完整源码:
```c
// SPDX-License-Identifier: GPL-2.0
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
char LICENSE[] SEC("license") = "GPL";
struct token_stats {
__u64 packets;
__u32 last_ifindex;
};
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 1);
__type(key, __u32);
__type(value, struct token_stats);
} stats_map SEC(".maps");
SEC("xdp")
int handle_packet(struct xdp_md *ctx)
{
struct token_stats *stats;
__u32 key = 0;
stats = bpf_map_lookup_elem(&stats_map, &key);
if (!stats)
return 0;
stats->packets++;
stats->last_ifindex = ctx->ingress_ifindex;
return XDP_PASS;
}
```
有几个设计选择值得注意:
**`BPF_MAP_TYPE_ARRAY`** 被选中是因为委托策略显式允许了 `array` map。如果我们改用 hash map,加载会失败,因为 token 不授予 `hash` map 的创建权限。这正是 token 模型在起作用:即使是微小的程序改动也会被委托策略捕获。
**`SEC("xdp")`** 匹配 `delegate_progs=xdp` 策略。如果你把它改成 `SEC("kprobe/...")`,内核会在加载时返回 `EPERM` 拒绝,因为 kprobe 不在允许的程序类型中。
**`XDP_PASS`** 简单地放行每个包。这个程序的唯一目的是证明基于 token 的加载和挂载成功了。在生产环境中,你会用真正的包处理逻辑来替换它。
## 用户态加载器:基于 Token 的加载
`token_trace.c` 加载器是一个标准的 libbpf skeleton 程序,唯一的关键区别是它传递了 `bpf_token_path`:
```c
struct bpf_object_open_opts open_opts = {};
open_opts.sz = sizeof(open_opts);
open_opts.bpf_token_path = env.token_path;
skel = token_trace_bpf__open_opts(&open_opts);
```
从这一刻开始,libbpf 接管了一切。当它调用 `bpf(BPF_MAP_CREATE)` 创建 `stats_map` 时,会附带 token fd。当它调用 `bpf(BPF_PROG_LOAD)` 加载 XDP 程序时,附带 token fd。当它调用 `bpf(BPF_LINK_CREATE)` 挂载到接口时,同样附带 token fd。
加载器的其余部分是标准流程:
```c
err = token_trace_bpf__load(skel); // token 用于 map_create + prog_load
link = bpf_program__attach_xdp(skel->progs.handle_packet, ifindex); // token 用于 link_create
```
挂载完成后,加载器在发送测试数据包前后分别读取 map 值来验证程序执行了:
```c
err = bpf_map_lookup_elem(map_fd, &key, &before);
// ... 向 127.0.0.1 发送 UDP 包 ...
err = bpf_map_lookup_elem(map_fd, &key, &after);
printf("delta : %llu\n", after.packets - before.packets);
```
如果 delta 是 1,说明 XDP 程序已经用委托的 capability 成功加载和挂载了。
## Namespace 编排器:`token_userns_demo`
由于 BPF token 要求非 init 的 user namespace,在主机上直接运行 `token_trace -t /sys/fs/bpf` 是行不通的。`token_userns_demo.c` 封装器自动处理了复杂的 namespace 编排。以下是完整流程:
### 第一步:Fork 并创建 Namespace
```
父进程 (root, init_user_ns) 子进程 (非特权, 新 userns)
│ │
│ fork() │
├────────────────────────────────────────>│
│ │
│ unshare(CLONE_NEWUSER)
│ unshare(CLONE_NEWNS | CLONE_NEWNET)
```
子进程创建新的 user namespace(在其中把自己映射为 uid/gid 0)、新的 mount namespace(使 bpffs 挂载是私有的)和新的 network namespace(使 `lo` 是一个全新的接口)。
### 第二步:创建 bpffs 并配置委托策略
```
父进程 (root, init_user_ns) 子进程 (新 userns)
│ │
│ fs_fd = fsopen("bpf", 0)
│ <───── 通过 SCM_RIGHTS 发送 fs_fd ──│
│ │
fsconfig(fs_fd, "delegate_cmds", ...) │ (等待确认)
fsconfig(fs_fd, "delegate_maps", "array") │
fsconfig(fs_fd, "delegate_progs", "xdp:...") │
fsconfig(fs_fd, "delegate_attachs", "any") │
fsconfig(fs_fd, FSCONFIG_CMD_CREATE) │
│ │
│ ───────── 发送确认 ─────────────────>│
```
子进程调用 `fsopen("bpf", 0)` 在自己的 user namespace 中创建一个 bpffs 文件系统上下文,然后通过 Unix socket(`SCM_RIGHTS`)把文件描述符发给父进程。父进程以 root 身份运行在 init namespace 中,用 `fsconfig()` 配置委托策略,然后用 `FSCONFIG_CMD_CREATE` 实例化文件系统。
这个两步配合是必要的,因为:(a) bpffs 必须在子进程的 user namespace 中创建(token 才能在那里有效),但 (b) 只有特权父进程才能设置委托选项(因为这些选项授予 BPF capability)。
### 第三步:挂载并加载
```
子进程 (新 userns)
│
mnt_fd = fsmount(fs_fd, 0, 0)
token_path = "/proc/self/fd/<mnt_fd>"
set_loopback_up()
exec("./token_trace", "-t", token_path, "-i", "lo")
```
子进程将 bpffs 实例化为一个分离的挂载(不需要挂载点,因为 `/proc/self/fd/<mnt_fd>` 提供了路径),在自己的 network namespace 中拉起 loopback 接口,然后 `exec` 执行 `token_trace` 并传入 bpffs 路径。从 `token_trace` 的角度看,它只是在用一个 token path 打开 BPF 对象,完全不知道也不关心 namespace 的设置过程。
## 手动准备 bpffs 挂载
如果你想在 demo 封装器之外试验 mount 语法,仓库里包含一个辅助脚本:
```bash
cd bpf-developer-tutorial/src/features/bpf_token
bash setup_token_bpffs.sh /tmp/bpf-token
```
它会在 `/tmp/bpf-token` 上用以下策略挂载 bpffs:
```text
delegate_cmds=prog_load:map_create:btf_load:link_create
delegate_maps=array
delegate_progs=xdp:socket_filter
delegate_attachs=any
```
**为什么要 `socket_filter`?** libbpf 在加载真正的 BPF 对象之前会做一次微小的 program-load probe 来检测内核特性支持。这个 probe 使用的是通用的 `BPF_PROG_TYPE_SOCKET_FILTER` 程序类型。如果委托策略中没有 `socket_filter`,probe 会失败,libbpf 拒绝继续。
**为什么要 `delegate_attachs=any`?** 同样的 libbpf probe 路径还会触发内核 token 检查代码中的 attach-type 验证。使用 `any` 避免了为 probe 兼容性而逐一列举每个可能的 attach type。
注意:这样的主机 namespace 挂载对于检查委托策略很有用(例如配合 `bpftool token list`),但除非 `bpf(BPF_TOKEN_CREATE)` syscall 来自匹配的非 init user namespace,否则不会产生可用的 token。
## 编译和运行
编译所有二进制文件:
```bash
cd bpf-developer-tutorial/src/features/bpf_token
make
```
运行端到端 demo:
```bash
sudo ./token_userns_demo
```
预期输出:
```text
token path : /proc/self/fd/5
interface : lo (ifindex=1)
packets before : 0
packets after : 1
delta : 1
last ifindex : 1
```
`delta: 1` 确认 XDP 程序已使用 BPF token 成功加载和挂载,子进程中没有 `CAP_BPF` 或 `CAP_SYS_ADMIN`。
加 `-v` 可以看到 libbpf 的详细输出,显示 token 的创建和使用过程:
```bash
sudo ./token_userns_demo -v
```
如果你自己已经管理好了在 user namespace 中的委托 bpffs,可以直接运行加载器:
```bash
./token_trace -t /proc/self/fd/<mnt-fd> -i lo
```
## 实际应用场景
虽然本教程使用了一个最小的 XDP 程序,但 BPF token 模式可以扩展到生产场景:
- **容器运行时**(LXD、Docker、Kubernetes):把带有特定 program 和 map 类型限制的委托 bpffs 挂载到容器中。LXD 已经通过 `security.delegate_bpf` 选项支持了这一点。
- **CI/CD 测试**:赋予构建任务加载和测试特定 eBPF 程序的能力,无需授予主机级 capability。委托策略充当 BPF 操作的白名单。
- **多租户 BPF 平台**:平台守护进程为每个租户创建不同委托策略的 bpffs 挂载。一个租户可能被允许使用 XDP + array map,另一个可能获得 tracepoint + ringbuf 访问权限。
- **LSM 集成**:由于 BPF token 和 Linux Security Module 集成,你可以将 token 委托和 SELinux 或 AppArmor 策略结合实现纵深防御。每个 token 获得自己的安全上下文,LSM hook 可以对其进行检查。
## 总结
本教程介绍了 BPF token 如何为 eBPF 权限提供一种超越 Linux capability "全有或全无"二元模型的委托机制。我们完整走过了整个流程:特权父进程用特定委托选项配置 bpffs 实例,user namespace 中的非特权子进程从该 bpffs 派生 token,libbpf 透明地使用 token 进行 map 创建、程序加载和挂载。最终结果是一个最小的 XDP 程序在非特权上下文中运行,这在 Linux 6.9 之前是不可能的。
BPF token 不是一个冷门功能。它代表了内核对 eBPF 生态系统中一个基本问题的回答:**在多租户环境中,如何安全地共享 BPF 能力,而不授予对 BPF 子系统的无约束访问?**
如果你想了解更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 <https://github.com/eunomia-bpf/bpf-developer-tutorial> 或网站 <https://eunomia.dev/tutorials/> 获取更多示例和完整教程。
## 参考
- [BPF Token 概念文档](https://docs.ebpf.io/linux/concepts/token/)
- [BPF token 内核补丁系列(Andrii Nakryiko)](https://lore.kernel.org/bpf/20240103222034.2582628-1-andrii@kernel.org/T/)
- [BPF token LWN 文章](https://lwn.net/Articles/959350/)
- [更细粒度的 BPF token LWN 讨论](https://lwn.net/Articles/947173/)
- [使用 BPF Token 进行权限委托(LXD 文档)](https://documentation.ubuntu.com/lxd/latest/explanation/bpf/)
- [bpf_token_create() libbpf API](https://docs.ebpf.io/ebpf-library/libbpf/userspace/bpf_token_create/)
- <https://docs.kernel.org/bpf/>

View File

@@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
MOUNTPOINT="${1:-/tmp/bpf-token}"
OPTIONS="delegate_cmds=prog_load:map_create:btf_load:link_create,delegate_maps=array,delegate_progs=xdp:socket_filter,delegate_attachs=any"
mkdir -p "${MOUNTPOINT}"
if mountpoint -q "${MOUNTPOINT}"; then
echo "bpffs is already mounted at ${MOUNTPOINT}"
exit 0
fi
mount -t bpf bpf "${MOUNTPOINT}" -o "${OPTIONS}"
echo "Mounted delegated bpffs at ${MOUNTPOINT}"
echo "Note: a bpffs mount in init_user_ns is useful for inspection, but token creation itself must happen from the same non-init user namespace as the bpffs instance."
grep " ${MOUNTPOINT} " /proc/mounts || true

View File

@@ -0,0 +1,32 @@
// SPDX-License-Identifier: GPL-2.0
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
char LICENSE[] SEC("license") = "GPL";
struct token_stats {
__u64 packets;
__u32 last_ifindex;
};
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 1);
__type(key, __u32);
__type(value, struct token_stats);
} stats_map SEC(".maps");
SEC("xdp")
int handle_packet(struct xdp_md *ctx)
{
struct token_stats *stats;
__u32 key = 0;
stats = bpf_map_lookup_elem(&stats_map, &key);
if (!stats)
return 0;
stats->packets++;
stats->last_ifindex = ctx->ingress_ifindex;
return XDP_PASS;
}

View File

@@ -0,0 +1,193 @@
// SPDX-License-Identifier: GPL-2.0
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <net/if.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#include <bpf/bpf.h>
#include <bpf/libbpf.h>
#include "token_trace.skel.h"
struct token_stats {
__u64 packets;
__u32 last_ifindex;
};
static struct env {
const char *token_path;
const char *ifname;
bool verbose;
bool no_trigger;
} env = {
.ifname = "lo",
};
static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
if (level == LIBBPF_DEBUG && !env.verbose)
return 0;
return vfprintf(stderr, format, args);
}
static void usage(const char *prog)
{
fprintf(stderr,
"Usage: %s [-t TOKEN_BPFFS] [-i IFACE] [-v] [-n]\n"
" -t TOKEN_BPFFS delegated bpffs mount used to derive a BPF token\n"
" -i IFACE interface to attach XDP program to (default: lo)\n"
" -v enable libbpf debug logs\n"
" -n do not generate loopback traffic automatically\n",
prog);
}
static int parse_args(int argc, char **argv)
{
int opt;
while ((opt = getopt(argc, argv, "t:i:vn")) != -1) {
switch (opt) {
case 't':
env.token_path = optarg;
break;
case 'i':
env.ifname = optarg;
break;
case 'v':
env.verbose = true;
break;
case 'n':
env.no_trigger = true;
break;
default:
return -EINVAL;
}
}
return 0;
}
static int generate_loopback_traffic(void)
{
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(9),
};
const char payload[] = "bpf token xdp demo";
int fd, err = 0;
if (inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr) != 1)
return -EINVAL;
fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0)
return -errno;
if (sendto(fd, payload, sizeof(payload), 0,
(struct sockaddr *)&addr, sizeof(addr)) < 0)
err = -errno;
close(fd);
return err;
}
int main(int argc, char **argv)
{
struct token_trace_bpf *skel = NULL;
struct bpf_object_open_opts open_opts = {};
struct token_stats before = {}, after = {};
struct bpf_link *link = NULL;
__u32 key = 0;
int ifindex, map_fd;
int err = 0;
err = parse_args(argc, argv);
if (err) {
usage(argv[0]);
return 1;
}
libbpf_set_print(libbpf_print_fn);
libbpf_set_memlock_rlim(0);
ifindex = if_nametoindex(env.ifname);
if (!ifindex) {
fprintf(stderr, "unknown interface '%s'\n", env.ifname);
return 1;
}
open_opts.sz = sizeof(open_opts);
open_opts.bpf_token_path = env.token_path;
skel = token_trace_bpf__open_opts(&open_opts);
if (!skel) {
fprintf(stderr, "failed to open token_trace skeleton\n");
return 1;
}
err = token_trace_bpf__load(skel);
if (err) {
fprintf(stderr,
"failed to load BPF program: %s\n"
"hint: if you intended to use a delegated token, pass -t <bpffs-path>\n",
strerror(-err));
goto cleanup;
}
link = bpf_program__attach_xdp(skel->progs.handle_packet, ifindex);
err = libbpf_get_error(link);
if (err) {
link = NULL;
fprintf(stderr, "failed to attach XDP program: %s\n", strerror(-err));
goto cleanup;
}
map_fd = bpf_map__fd(skel->maps.stats_map);
err = bpf_map_lookup_elem(map_fd, &key, &before);
if (err) {
err = -errno;
fprintf(stderr, "failed to read stats before traffic: %s\n",
strerror(errno));
goto cleanup;
}
if (!env.no_trigger && strcmp(env.ifname, "lo") == 0) {
err = generate_loopback_traffic();
if (err) {
fprintf(stderr, "failed to generate loopback traffic: %s\n",
strerror(-err));
goto cleanup;
}
usleep(100000);
} else if (!env.no_trigger) {
printf("Generate traffic on %s and re-run with -n if you only want attach/query.\n",
env.ifname);
}
err = bpf_map_lookup_elem(map_fd, &key, &after);
if (err) {
err = -errno;
fprintf(stderr, "failed to read stats after traffic: %s\n",
strerror(errno));
goto cleanup;
}
printf("token path : %s\n",
env.token_path ? env.token_path :
"(none, libbpf may use LIBBPF_BPF_TOKEN_PATH or /sys/fs/bpf)");
printf("interface : %s (ifindex=%d)\n", env.ifname, ifindex);
printf("packets before : %llu\n", (unsigned long long)before.packets);
printf("packets after : %llu\n", (unsigned long long)after.packets);
printf("delta : %llu\n",
(unsigned long long)(after.packets - before.packets));
printf("last ifindex : %u\n", after.last_ifindex);
cleanup:
bpf_link__destroy(link);
token_trace_bpf__destroy(skel);
return err != 0;
}

View File

@@ -0,0 +1,452 @@
// SPDX-License-Identifier: GPL-2.0
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <linux/mount.h>
#include <net/if.h>
#include <sched.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mount.h>
#include <sys/resource.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <unistd.h>
static struct env {
bool verbose;
bool no_trigger;
} env = {
};
static void usage(const char *prog)
{
fprintf(stderr,
"Usage: %s [-v] [-n]\n"
" -v enable verbose token_trace logs\n"
" -n do not generate loopback traffic automatically\n",
prog);
}
static int parse_args(int argc, char **argv)
{
int opt;
while ((opt = getopt(argc, argv, "vn")) != -1) {
switch (opt) {
case 'v':
env.verbose = true;
break;
case 'n':
env.no_trigger = true;
break;
default:
return -EINVAL;
}
}
return 0;
}
static inline int sys_fsopen(const char *fsname, unsigned flags)
{
return syscall(__NR_fsopen, fsname, flags);
}
static inline int sys_fsconfig(int fs_fd, unsigned cmd, const char *key,
const void *val, int aux)
{
return syscall(__NR_fsconfig, fs_fd, cmd, key, val, aux);
}
static inline int sys_fsmount(int fs_fd, unsigned flags, unsigned ms_flags)
{
return syscall(__NR_fsmount, fs_fd, flags, ms_flags);
}
static ssize_t write_nointr(int fd, const void *buf, size_t count)
{
ssize_t ret;
do {
ret = write(fd, buf, count);
} while (ret < 0 && errno == EINTR);
return ret;
}
static int write_file(const char *path, const void *buf, size_t count)
{
int fd;
ssize_t ret;
fd = open(path, O_WRONLY | O_CLOEXEC | O_NOCTTY);
if (fd < 0)
return -errno;
ret = write_nointr(fd, buf, count);
close(fd);
if (ret < 0)
return -errno;
if ((size_t)ret != count)
return -EIO;
return 0;
}
static int sendfd(int sockfd, int fd)
{
struct msghdr msg = {};
struct cmsghdr *cmsg;
int fds[1] = { fd };
char iobuf[1] = { 0 };
struct iovec io = {
.iov_base = iobuf,
.iov_len = sizeof(iobuf),
};
union {
char buf[CMSG_SPACE(sizeof(fds))];
struct cmsghdr align;
} u = {};
ssize_t ret;
msg.msg_iov = &io;
msg.msg_iovlen = 1;
msg.msg_control = u.buf;
msg.msg_controllen = sizeof(u.buf);
cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(fds));
memcpy(CMSG_DATA(cmsg), fds, sizeof(fds));
ret = sendmsg(sockfd, &msg, 0);
if (ret < 0)
return -errno;
if (ret != 1)
return -EIO;
return 0;
}
static int recvfd(int sockfd, int *fd)
{
struct msghdr msg = {};
struct cmsghdr *cmsg;
int fds[1];
char iobuf[1];
struct iovec io = {
.iov_base = iobuf,
.iov_len = sizeof(iobuf),
};
union {
char buf[CMSG_SPACE(sizeof(fds))];
struct cmsghdr align;
} u = {};
ssize_t ret;
msg.msg_iov = &io;
msg.msg_iovlen = 1;
msg.msg_control = u.buf;
msg.msg_controllen = sizeof(u.buf);
ret = recvmsg(sockfd, &msg, 0);
if (ret < 0)
return -errno;
if (ret != 1)
return -EIO;
cmsg = CMSG_FIRSTHDR(&msg);
if (!cmsg)
return -EINVAL;
if (cmsg->cmsg_len != CMSG_LEN(sizeof(fds)))
return -EINVAL;
if (cmsg->cmsg_level != SOL_SOCKET || cmsg->cmsg_type != SCM_RIGHTS)
return -EINVAL;
memcpy(fds, CMSG_DATA(cmsg), sizeof(fds));
*fd = fds[0];
return 0;
}
static int create_and_enter_userns(void)
{
uid_t uid = getuid();
gid_t gid = getgid();
char map[64];
int err;
if (unshare(CLONE_NEWUSER))
return -errno;
err = write_file("/proc/self/setgroups", "deny", sizeof("deny") - 1);
if (err && err != -ENOENT)
return err;
snprintf(map, sizeof(map), "0 %d 1", uid);
err = write_file("/proc/self/uid_map", map, strlen(map));
if (err)
return err;
snprintf(map, sizeof(map), "0 %d 1", gid);
err = write_file("/proc/self/gid_map", map, strlen(map));
if (err)
return err;
if (setgid(0))
return -errno;
if (setuid(0))
return -errno;
return 0;
}
static int set_delegate_mask(int fs_fd, const char *key, const char *mask_str)
{
int err;
err = sys_fsconfig(fs_fd, FSCONFIG_SET_STRING, key, mask_str, 0);
if (err < 0)
return -errno;
return 0;
}
static int set_loopback_up(void)
{
struct ifreq ifr = {};
int fd;
fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0)
return -errno;
snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "lo");
if (ioctl(fd, SIOCGIFFLAGS, &ifr) < 0) {
close(fd);
return -errno;
}
ifr.ifr_flags |= IFF_UP | IFF_RUNNING;
if (ioctl(fd, SIOCSIFFLAGS, &ifr) < 0) {
close(fd);
return -errno;
}
close(fd);
return 0;
}
static void raise_memlock_limit(void)
{
struct rlimit rlim = {
.rlim_cur = RLIM_INFINITY,
.rlim_max = RLIM_INFINITY,
};
if (setrlimit(RLIMIT_MEMLOCK, &rlim))
fprintf(stderr, "warning: failed to raise RLIMIT_MEMLOCK: %s\n",
strerror(errno));
}
static int child_main(int sockfd)
{
char ack;
char token_path[64];
int err, fs_fd = -1, mnt_fd = -1;
err = create_and_enter_userns();
if (err) {
fprintf(stderr, "failed to create user namespace: %s\n",
strerror(-err));
return 1;
}
if (unshare(CLONE_NEWNS | CLONE_NEWNET)) {
err = -errno;
fprintf(stderr, "failed to create mount/net namespace: %s\n",
strerror(errno));
return 1;
}
if (mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)) {
err = -errno;
fprintf(stderr, "failed to remount / private: %s\n", strerror(errno));
return 1;
}
err = set_loopback_up();
if (err) {
fprintf(stderr, "failed to bring loopback up: %s\n",
strerror(-err));
return 1;
}
fs_fd = sys_fsopen("bpf", 0);
if (fs_fd < 0) {
err = -errno;
fprintf(stderr, "fsopen(\"bpf\") failed: %s\n", strerror(errno));
return 1;
}
err = sendfd(sockfd, fs_fd);
if (err) {
fprintf(stderr, "failed to send bpffs fs_fd: %s\n", strerror(-err));
goto out;
}
if (read(sockfd, &ack, 1) != 1) {
fprintf(stderr, "failed to receive parent ack\n");
err = -EIO;
goto out;
}
mnt_fd = sys_fsmount(fs_fd, 0, 0);
if (mnt_fd < 0) {
err = -errno;
fprintf(stderr, "fsmount() failed: %s\n", strerror(errno));
goto out;
}
snprintf(token_path, sizeof(token_path), "/proc/self/fd/%d", mnt_fd);
{
const char *argv[10];
int argc = 0;
argv[argc++] = "./token_trace";
if (env.verbose)
argv[argc++] = "-v";
if (env.no_trigger)
argv[argc++] = "-n";
argv[argc++] = "-t";
argv[argc++] = token_path;
argv[argc++] = "-i";
argv[argc++] = "lo";
argv[argc] = NULL;
execv("./token_trace", (char *const *)argv);
}
err = -errno;
fprintf(stderr, "failed to exec ./token_trace: %s\n", strerror(errno));
out:
if (mnt_fd >= 0)
close(mnt_fd);
if (fs_fd >= 0)
close(fs_fd);
return 1;
}
int main(int argc, char **argv)
{
static const char *delegate_cmds =
"prog_load:map_create:btf_load:link_create";
int err, socks[2] = { -1, -1 }, fs_fd = -1, status;
pid_t pid;
char ack = 1;
err = parse_args(argc, argv);
if (err) {
usage(argv[0]);
return 1;
}
if (geteuid() != 0) {
fprintf(stderr, "run this demo with sudo/root so the parent can configure delegated bpffs\n");
return 1;
}
if (access("./token_trace", X_OK) != 0) {
fprintf(stderr, "missing ./token_trace, run 'make' in this directory first\n");
return 1;
}
raise_memlock_limit();
if (socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0, socks)) {
fprintf(stderr, "socketpair failed: %s\n", strerror(errno));
return 1;
}
pid = fork();
if (pid < 0) {
fprintf(stderr, "fork failed: %s\n", strerror(errno));
return 1;
}
if (pid == 0) {
close(socks[0]);
return child_main(socks[1]);
}
close(socks[1]);
err = recvfd(socks[0], &fs_fd);
if (err) {
fprintf(stderr, "failed to receive bpffs fs_fd: %s\n", strerror(-err));
goto out;
}
err = set_delegate_mask(fs_fd, "delegate_cmds", delegate_cmds);
if (err) {
fprintf(stderr, "failed to set delegate_cmds: %s\n", strerror(-err));
goto out;
}
err = set_delegate_mask(fs_fd, "delegate_maps", "array");
if (err) {
fprintf(stderr, "failed to set delegate_maps: %s\n", strerror(-err));
goto out;
}
err = set_delegate_mask(fs_fd, "delegate_progs", "xdp:socket_filter");
if (err) {
fprintf(stderr, "failed to set delegate_progs: %s\n", strerror(-err));
goto out;
}
err = set_delegate_mask(fs_fd, "delegate_attachs", "any");
if (err) {
fprintf(stderr, "failed to set delegate_attachs: %s\n", strerror(-err));
goto out;
}
if (sys_fsconfig(fs_fd, FSCONFIG_CMD_CREATE, NULL, NULL, 0) < 0) {
err = -errno;
fprintf(stderr, "failed to materialize bpffs: %s\n", strerror(errno));
goto out;
}
if (write(socks[0], &ack, 1) != 1) {
err = -errno;
fprintf(stderr, "failed to send parent ack: %s\n", strerror(errno));
goto out;
}
err = 0;
out:
if (fs_fd >= 0)
close(fs_fd);
close(socks[0]);
if (waitpid(pid, &status, 0) < 0) {
fprintf(stderr, "waitpid failed: %s\n", strerror(errno));
return 1;
}
if (err)
return 1;
if (WIFEXITED(status))
return WEXITSTATUS(status);
if (WIFSIGNALED(status)) {
fprintf(stderr, "child terminated by signal %d\n", WTERMSIG(status));
return 1;
}
return 1;
}