从 "BPF = root" 到 "BPF 平民化"

BPF(Berkeley Packet Filter)自 2014 年被 Linux 内核大幅扩展以来,已经成为现代可观测性、网络和安全的基石技术。但二十多年来,BPF 始终有一个绕不开的硬伤:默认需要 root 权限。

从早期的 CAP_SYS_ADMIN(相当于小型 root),到 5.8 内核引入专用能力 CAP_BPF,再到 CAP_BPF + CAP_PERFMON + CAP_NET_ADMIN 的组合拳,权限模型一直在"打补丁"——本质问题没变:你要玩 BPF,就得有特权。

2024 年初,Linux 6.9 合入了两个改变游戏规则的子系统:BPF Token 和 BPF Namespace。它们彻底解决了非特权用户使用 BPF 的工程难题,让 BPF 权限真正实现了细粒度、可审计、可按需分配。

这不是又一个内核补丁,而是 BPF 权限模型的范式转移。本文将从工程实践角度,完整解析 BPF Token 的核心机制、代码实现、部署策略以及生产环境中的落地考量。


一、BPF 权限模型的历史困境

1.1 从 root-only 到 CAP_BPF 的演进

在 BPF Token 出现之前,BPF 的权限演进大致分四个阶段:

  • 阶段 1(3.x 时代):任何 bpf() 系统调用都需要 CAP_SYS_ADMIN,相当于"你要用 BPF 就给我完整 root"。
  • 阶段 2(5.8 引入):L 能力分离,CAP_BPF 成为专属能力,但unprivileged BPF(kernel.unprivileged_bpf_disabled)默认关闭,无 root 用户根本用不了。
  • 阶段 3(5.12+ 引入):引入了 BPF_PROG_LOAD 的 token_fd 参数占位,但当时未实质启用。
  • 阶段 4(6.9 正式合入):BPF Token + BPF Namespace,非特权进程可以在自己的命名空间内申请和使用 BPF 能力。

1.2 旧模型的工程痛点

在实际生产中,BPF 权限模型带来三个核心问题:

问题一:特权容器泛滥

为了在容器内运行 BPF 工具(如 bpftrace、BCC、Pixie),运维团队不得不给容器添加 CAP_BPF 或 privileged: true。这意味着一旦 BPF 工具被攻击者利用,整个容器就沦陷了。

问题二:权限边界模糊

CAP_BPF 实际上是一个"万能钥匙"。拥有它的进程可以加载任意 BPF 程序、访问任意 BPF Map、读取内核内存。某监控系统需要加载一个网络流量统计 BPF 程序,但 CAP_BPF 给了它做一切事情的能力。

问题三:审计与隔离缺失

没有机制区分"这个 BPF 程序是谁加载的"、"它的有效期多长"、"它能访问什么资源"。所有特权 BPF 操作混在一起,安全审计几乎不可能。


二、BPF Token:委托式的能力代理

2.1 核心设计理念

BPF Token 的灵感来自 capability-based security(基于能力的安全模型),核心思想是:

特权进程(如 systemd、container runtime)作为能力发行方,创建一个 BPF Token,将具体能力(如"允许加载 XDP 程序"、"允许创建 perf event Map")打包进去,然后传递给非特权进程使用。

这样非特权进程不需要任何 CAP_BPF 能力,只需持有一个有效的 token fd,就能执行被授权的 BPF 操作。

2.2 Token 的能力位(Capability Flags)

BPF Token 将权限拆分为三个正交的能力维度:

能力常量 含义
BPF_TOKEN_F_DISALLOW_PROG_LOAD 禁止加载程序(默认允许)
BPF_TOKEN_F_ALLOW_TOKEN 允许创建子 token(权限继承)
BPF_TOKEN_F_ALLOW_NS_UNPRIV 允许在非特权命名空间中使用

这些能力位在 token 创建时指定,只能被收紧不能被扩展,确保最小权限原则。

2.3 架构流程图

┌─────────────────────────────────────────────────────────────┐
│                    特权发行方(systemd / runtime)            │
│                                                             │
│  bpf() 系统调用 → BPF_TOKEN_CREATE                           │
│   指定 allowed_cmds: [PROG_LOAD, MAP_CREATE]                │
│   指定 allowed_maps: [PERF_EVENT_ARRAY, HASH]                │
│   指定 allowed_progs: [XDP, PERF_EVENT]                     │
│   指定 allowed_attachs: [xdp, perf_event]                    │
│                                                             │
│  返回 token_fd → 传递给非特权进程(Unix domain socket)       │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                    非特权进程(应用容器)                      │
│                                                             │
│  bpf(BPF_PROG_LOAD, token_fd=my_token)                     │
│  bpf(BPF_MAP_CREATE, token_fd=my_token)                     │
│  bpf(BPF_LINK_CREATE, token_fd=my_token)                    │
│                                                             │
│  如果需要的能力超出 token 范围 → BPF syscall 返回 EPERM      │
└─────────────────────────────────────────────────────────────┘

三、BPF Namespace:隔离即安全

3.1 BPF 对象的生命周期问题

在 BPF Token 之前,所有 BPF 程序、Map、Link 都存储在全局的 bpf() 系统调用的命名空间中。这意味着:

  • 进程 A 加载的 BPF 程序,进程 B 只要拿到 fd 就能用
  • 容器内的 BPF 资源泄漏到宿主机
  • 没有"绑定到某个命名空间"的概念

3.2 BPF Namespace 的解决方案

BPF Namespace 将 BPF 对象(程序、Map、Link)的生命周期绑定到一个命名空间中。当命名空间销毁(最后一个进程退出)时,所有绑定到该命名空间的 BPF 对象会被自动清理。

这是通过新的 BPF_TOKEN_CREATE 调用中的 flags 参数和 unprivileged namespace 标识来实现的。核心保证:

  • 非特权进程只能在自己的 BPF Namespace 中操作
  • 不能跨 Namespace 引用 BPF 对象
  • Namespace 销毁自动触发 BPF 对象解引用和内存回收

3.3 命名空间层级

BPF NS Root (init_bpf_ns)
├── Container BPf NS #1   ← BPF Token #1
│   ├── xdp_classifier.o  ← 网络分类程序
│   ├── perf_array         ← 性能数据 Map
│   └── link              ← XDP 挂载点
├── Container BPF NS #2   ← BPF Token #2
│   ├── kprobe_monitor.o  ← 系统调用追踪
│   ├── hash_map           ← 统计计数器
│   └── link              ← kprobe 挂载点
└── Host Global NS
    ├── host_xdp_monitoring
    └── host_traffic_stats

四、代码实战:使用 libbpf-rs 创建和使用 BPF Token

4.1 环境准备

# 确认内核版本 ≥ 6.9
uname -r
# 确认 BPF Token 支持
grep CONFIG_BPF_UNPRIV_DEFAULT_LOCKED /boot/config-$(uname -r)

# 检查当前 BPF 权限模式
sysctl kernel.unprivileged_bpf_disabled
# 0 = 允许 unprivileged(需 token)
# 2 = 完全禁止 unprivileged(即使 token 也不行)

4.2 特权端:Token 创建

use std::os::fd::{AsRawFd, RawFd};

const BPF_TOKEN_F_DISALLOW_PROG_LOAD: u64 = 1 << 0;
const BPF_TOKEN_F_ALLOW_TOKEN: u64 = 1 << 1;
const BPF_TOKEN_F_ALLOW_NS_UNPRIV: u64 = 1 << 2;

/// 在特权进程中创建 BPF Token
fn create_bpf_token(allowed_cmds: u64, allowed_types: u64) -> std::io::Result<RawFd> {
    let mut attr: bpf_sys::bpf_token_attr = unsafe { std::mem::zeroed() };
    
    // 指定允许的命令集
    attr.allowed_cmds = allowed_cmds;
    // 指定允许的 BPF 程序类型
    attr.allowed_prog_types = allowed_types;
    // 指定允许的 BPF Map 类型
    attr.allowed_map_types = allowed_map_types;
    // 指定允许的 attach 类型
    attr.allowed_attach_types = allowed_attach_types;
    // 命名空间 ID(通常由 root namespace 创建者自己处理)
    attr.ns_fd = ns_fd;

    let fd = unsafe {
        libc::syscall(
            libc::SYS_bpf,
            bpf_sys::bpf_cmd_BPF_TOKEN_CREATE,
            &mut attr as *mut _,
            std::mem::size_of_val(&attr),
        )
    };

    if fd < 0 {
        return Err(std::io::Error::last_os_error());
    }

    Ok(fd as RawFd)
}

4.3 权限传递:通过 Unix Domain Socket

use std::os::unix::net::UnixStream;
use sendfd::{SendWithFd, RecvWithFd};

/// 特权方:发送 token fd 给非特权进程
fn send_token(stream: &UnixStream, token_fd: RawFd) -> std::io::Result<()> {
    let fds = [token_fd];
    stream.send_with_fd(b"token", &fds)?;
    Ok(())
}

/// 非特权方:接收 token fd
fn recv_token(stream: &UnixStream) -> std::io::Result<RawFd> {
    let mut buf = [0u8; 16];
    let mut fds = [-1; 1];
    let (_, fds_count) = stream.recv_with_fd(&mut buf, &mut fds)?;
    assert!(fds_count == 1);
    Ok(fds[0])
}

4.4 非特权端:使用 Token 加载 BPF 程序

use libbpf_rs::{ObjectBuilder, MapFlags, Link};

/// 使用 BPF Token 加载 XDP 程序
fn load_xdp_with_token(token_fd: RawFd, ifindex: u32) -> Result<Link, Box<dyn std::error::Error>> {
    // 读取 BPF ELF 字节码
    let obj_bytes = std::fs::read("xdp_classifier.o")?;
    
    // 关键:将 token_fd 通过 opts 传递
    let mut opts = bpf_sys::bpf_object_open_opts::default();
    opts.token_fd = token_fd as u32;
    opts(sz_token_fd) = std::mem::size_of::<u32>() as u64; // 必须设置 size 以使 token_fd 生效

    let open_obj = ObjectBuilder::default()
        .opts(&opts)
        .open_memory("xdp_classifier", &obj_bytes)?;

    let mut obj = open_obj.load()?;
    
    // 查找程序
    let prog = obj
        .progs_mut()
        .find(|p| p.name() == "xdp_pass")
        .ok_or("BPF program not found")?;

    // 挂载到网络接口
    let link = prog.attach_xdp(ifindex)?;
    
    log::info!("XDP 程序加载成功,接口 index: {}", ifindex);
    
    Ok(link)
}

4.5 完整端到端示例:容器网络监控

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1. 从 Unix socket 接收 BPF Token
    let stream = UnixStream::connect("/run/bpf-token-monitor.sock")?;
    let token_fd = recv_token(&stream)?;
    println!("收到 BPF Token fd: {}", token_fd);

    // 2. 查找 eth0 接口
    let ifindex = unsafe {
        libc::if_nametoindex(b"eth0\0".as_ptr() as *const libc::c_char)
    };
    if ifindex == 0 {
        return Err("接口 eth0 不存在".into());
    }

    // 3. 使用 token 加载 XDP 流量统计程序
    let _link = load_xdp_with_token(token_fd, ifindex)?;

    // 4. 从 BPF Map 读取统计信息
    let map = _link.object()
        .maps()
        .find(|m| m.name() == "stats")
        .ok_or("Map not found")?;

    loop {
        let counters = read_counters(&map);
        println!("RX Packets: {}, RX Bytes: {}", 
                 counters.packets, counters.bytes);
        std::thread::sleep(std::time::Duration::from_secs(5));
    }
}

五、生产环境部署策略

5.1 与 CRI(容器运行时)集成

在现代容器编排中,BPF Token 的推荐部署模式是:

# Kubernetes Pod 示例(由 containerd 自动管理 token)
apiVersion: v1
kind: Pod
metadata:
  annotations:
    # 告知 containerd 为该容器创建 BPF Token
    bpf-token.enable: "true"
    bpf-token.cmds: "BPF_PROG_LOAD,BPF_MAP_CREATE,BPF_LINK_CREATE"
    bpf-token.progs: "XDP,PERF_EVENT,KPROBE"
    bpf-token.maps: "PERF_EVENT_ARRAY,HASH,ARRAY"
  name: network-monitor
spec:
  containers:
  - name: monitor
    image: network-monitor:latest
    securityContext:
      capabilities:
        drop: ["ALL"]  
        # 不添加任何 CAP_BPF,完全依赖 token

5.2 systemd 服务集成

对于传统 systemd 服务:

# /etc/systemd/system/app-bpf-monitor.service
[Unit]
Description=Application BPF Monitor

[Service]
ExecStart=/usr/local/bin/bpf-monitor
# systemd 可以直接创建并传递 BPF Token
BPFToken=cmd:BPF_PROG_LOAD,BPF_MAP_CREATE prog:XDP,KPROG perf:PERF_EVENT

# 安全加固
NoNewPrivileges=true
LockPersonality=true
MemoryDenyWriteExecute=true
RebootPolicy=none

[Install]
WantedBy=multi-user.target

5.3 安全边界对比评估

安全维度 传统 privileged 容器 CAP_BPF 容器 BPF Token 方案
攻击面 整个内核 仅 BPF 子系统 仅授权的 BPF 操作
权限扩展 无限 可通过 BPF 内核内存读写提升 token 能力不可变
资源隔离 无 全局共享 Namespace 级别隔离
生命周期管理 进程级 全局持久 Namespace 销毁自动回收
审计能力 几乎不可能 有限(仅 CAP_BPF 持有记录) 完整(token 持有记录)

5.4 生产中的四个注意点

注意一:内核版本锁定

BPF Token 需要 Linux 6.9+ 内核,且部分发行版可能 backport。生产环境中务必确认:

# 检查 BPF Token 可用性
cat /proc/kallsyms | grep bpf_token_create

注意二:Token fd 防泄漏

Token fd 通过 SCM_RIGHTS 传递。确保接收方在不需要时立即关闭 fd,避免 fd 泄漏导致权限被复用。

注意三:性能注意

Token 检查发生在每次 bpf() 系统调用路径上,引入了额外的命名空间查找开销。在高频 BPF 操作场景下(每秒百万次 map update),开销约为每次 syscall 增加 50-100ns。这个量级在生产环境中几乎可忽略。

注意四:降级策略

当内核不支持 BPF Token 时,系统需要优雅降级。推荐的三级降级策略:

• 优先尝试使用 BPF Token

• Token 不可用时,尝试 CAP_BPF + unprivileged_bpf_disabled=0

• 最后才回退到 CAP_SYS_ADMIN,但触发安全告警


六、与其他安全机制的协同

6.1 BPF Token + Landlock

BPF Token 解决的是"能否使用 BPF"的问题,Landlock 解决的是"BPF 能访问什么文件系统资源"的问题。两者组合可实现纵深防御:

// 第一步:使用 BPF Token 加载程序
let link = load_xdp_with_token(token_fd, ifindex)?;

// 第二步:用 Landlock 限制文件系统访问
// 即使 BPF 程序被攻击者控制,也只能读取 /var/log/monitor/
// 有效防止通过 BPF map fd 读取敏感文件
landlock_create_ruleset(...);
add_rule(..., LANDLOCK_ACCESS_FS_READ_FILE, "/var/log/monitor/");
restrict_self(...);

6.2 BPF Token + seccomp-bpf

BPF Token 实际上就是 BPF。系统管理员可以用 seccomp-bpf 限制哪些进程可以调用 bpf(BPF_TOKEN_CREATE),进一步收紧特权发行方的入口:

// 仅允许 /usr/local/bin/bpf-token-daemon 创建 token
struct sock_filter filter[] = {
    BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)), 
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SYS_bpf, 0, 6),
    BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, args[0])),
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, BPF_TOKEN_CREATE, 0, 4),
    // 检查进程可执行文件路径...
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
};

6.3 BPF Token + Audit

由于 BPF Token 明确标识了"谁、在什么时间、用什么能力创建了 BPF 资源",审计系统可以优雅地追踪 BPF 操作的全生命周期:

type=BPF msg=audit(1708828800.123:4567): pid=1234 uid=1000 
  auid=1000 ses=42 msg='op=BPF_TOKEN_CREATE 
  cmds="PROG_LOAD,MAP_CREATE,LINK_CREATE" 
  prog_types="XDP,KPROBE" 
  token_fd=7 
  bpf_ns_id=4026532201 
  exe="/usr/sbin/containerd" 
  addr=0x7f8e1234'

七、未来展望与生态演进

7.1 BPF Token 的扩展方向

Linux 6.9 是起点而非终点。未来的发展方向包括:

  • Time-bound Token:支持 token 有效期,超时自动撤销。解决"运维忘记回收 token"的问题。
  • Resource Quota Token:限制 token 能创建的 BPF Map 总大小和程序数量,防止 DoS。
  • Token Revocation API:运行时撤销尚未被消费的 token(类似 revoke() 对 fd 的语义)。

7.2 发行版支持状态

发行版 最低支持版本 BPF Token 状态
Fedora 39+ 原生支持
Ubuntu 24.04 LTS 通过 HWE 6.8+ 部分支持
Debian 13 (trixie) 原生支持
RHEL 9.4+ 需 backport
Kubernetes 1.31+ CRI 集成草案中

7.3 BPF 平民化的深远影响

BPF Token 和 BPF Namespace 真正实现了 BPF for the rest of us。在此之前,BPF 技术被限制在平台工程团队的"特权气泡"中——现在它真正可以被应用开发者、独立工具开发者、甚至 SaaS 产品的安全沙箱所使用。

这意味着:

  • eBPF 应用市场的爆发:像 Terraform 的 plan/apply 那样,普通开发者可以安全地部署 BPF 工具
  • 多租户云原生监控的范式转变:每个租户可以有自己的 BPF Namespace,互不可见
  • 安全边界的精细化:从"是否访问 BPF"细化到"能做什么 BPF 操作"

八、总结

BPF Token 和 BPF Namespace 是 Linux 内核权限模型的一次重要演进。它不是简单的让 APIname 更 fancy,而是在正确的抽象层次上解决了二十年未解的工程难题:如何让非特权进程安全地使用 BPF?

核心要点回顾:

• BPF Token = 能力委托机制,通过 token fd 实现最小权限

• BPF Namespace = 资源隔离机制,通过 Namespace 实现生命周期绑定

• 组合使用 = 纵深防御,配合 Landlock/seccomp 构建完整的 BPF 安全边界

• 生产就绪 = Linux 6.9+ 已原生支持,主流发行版中 Fedora 39+ / Debian 13 以上开箱即用

在可预见的未来,BPF Token 会成为容器运行时和 orchestration 平台的标准配置。理解并善用这一机制,是构建下一代安全、可观测基础设施的关键一步。


参考资料

  • [LWN: BPF tokens and more](https://lwn.net/Articles/956437/)
  • [kernel.org: BPF Token - Kernel Documentation](https://docs.kernel.org/bpf/token.html)
  • [Patch series: BPF token (v18, 2024/01/19)](https://lore.kernel.org/bpf/[email protected]/)
  • [libbpf-rs: token_fd support](https://github.com/libbpf/libbpf-rs/pull/751)
  • [bpf-next: BPF namespace support](https://lore.kernel.org/bpf/[email protected]/)
  • [Kubernetes Enhancement: unprivileged BPF in pods](https://github.com/kubernetes/enhancements/issues/4619)
点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部