2. eBPF基础回顾:程序类型、Map机制与安全模型

在深入租户隔离蓝图之前,必须牢固掌握eBPF的核心基础。本章聚焦三大支柱:程序类型(决定挂载点与触发时机)、Map机制(内核与用户态的数据交换通道)以及验证器与安全模型(保障系统稳定性的第一道防线)。

2.1 eBPF程序类型全景

eBPF程序通过挂载到内核的不同事件点来执行。每种类型决定了程序能访问的数据、返回值的含义以及执行上下文。下表列出多租户场景中最关键的几种类型:

程序类型 挂载点 触发时机 典型租户隔离用途
XDP 网卡驱动层(最早) 每个入站数据包 租户流量过滤、DDoS防护、早期丢弃
TC(cls_bpf) 内核网络协议栈 数据包入/出队列 租户带宽整形、QoS标记、流量镜像
Kprobe 内核函数入口/出口 函数调用时 监控租户进程系统调用、审计文件操作
Tracepoint 内核静态跟踪点 预定义事件发生时 容器创建/销毁事件、网络命名空间切换
Socket Filter Socket层 数据包收发 租户应用级协议过滤、socket权限控制
cgroup BPF cgroup层级 进程/设备/网络操作 租户资源隔离、设备访问控制、套接字绑定限制
关键选择原则: 对于网络隔离场景,优先选择 XDP(性能最高,但只能处理入站)或 TC(双向处理,功能更丰富)。对于系统调用审计,Kprobe 提供最大灵活性,但 Tracepoint 更稳定且受验证器限制更少。

2.2 eBPF Map机制:数据交换的核心

Map是eBPF程序与用户态控制平面之间、以及不同eBPF程序之间共享数据的唯一合法通道。Map类型决定了数据组织方式和并发访问语义。

2.2.1 常用Map类型对比

Map类型 数据结构 并发安全 典型场景
BPF_MAP_TYPE_HASH 哈希表(键值对) 需自旋锁保护 租户配置表、会话状态、黑名单
BPF_MAP_TYPE_ARRAY 固定大小数组 无锁(索引访问) 统计计数器、预分配规则表
BPF_MAP_TYPE_PERCPU_HASH 每CPU哈希表 天然无锁(per-CPU) 高性能租户流量统计、延迟直方图
BPF_MAP_TYPE_RINGBUF 环形缓冲区 多生产者/单消费者 租户事件日志、审计记录上报
BPF_MAP_TYPE_STACK_TRACE 栈跟踪存储 只读 租户异常调用栈记录

2.2.2 Map操作生命周期

  1. 创建(用户态): 通过 bpf(BPF_MAP_CREATE, ...) 系统调用指定类型、键值大小、最大条目数。
  2. 加载(用户态→内核): eBPF程序通过 SEC(".maps") 引用Map,加载器自动关联。
  3. 访问(内核态): 程序内使用 bpf_map_lookup_elem()、bpf_map_update_elem() 等辅助函数。
  4. 控制(用户态): 控制平面通过 bpf(BPF_MAP_LOOKUP_ELEM, ...) 读取或更新Map内容。
  5. 销毁(用户态): 关闭Map文件描述符,内核自动回收。
租户隔离关键点: 每个租户应拥有独立的Map实例(或使用Map中的租户ID字段做逻辑隔离)。PERCPU 类型在统计场景下能避免锁竞争,但需要注意CPU热插拔时的数据一致性。

2.3 eBPF验证器与安全模型

验证器是eBPF安全性的基石。它确保所有加载到内核的程序不会导致系统崩溃、死锁或信息泄露。理解验证器的约束,是编写正确且安全的租户隔离程序的前提。

2.3.1 验证器核心检查项

  • 控制流完整性(CFI): 程序必须是有向无环图(DAG),禁止循环(除非有界循环被展开)。
  • 指针安全: 禁止解引用未验证的指针;禁止将指针泄漏到用户态;禁止算术运算修改指针类型。
  • 栈边界检查: 栈访问必须在 MAX_BPF_STACK(512字节)范围内。
  • 辅助函数白名单: 只能调用验证器允许的辅助函数(如 bpf_get_current_pid_tgid、bpf_skb_load_bytes 等)。
  • Map访问验证: 键值大小必须与Map定义匹配;不能越界访问数组Map。
  • 尾调用限制: 尾调用链深度不超过 MAX_TAIL_CALL_CNT(通常33层)。

2.3.2 安全模型层级

层级 机制 防护目标
L1:验证器 静态分析 + 符号执行 防止恶意/错误程序破坏内核
L2:CAP_BPF Linux Capability 限制谁可以加载eBPF程序
L3:权限掩码 程序类型 + Map类型权限 限制程序能访问的资源
L4:LSM BPF Linux Security Module钩子 实现细粒度的强制访问控制
常见陷阱: 即使通过验证器,程序仍可能因 Map竞争条件 或 辅助函数误用 导致数据不一致。多租户场景下,务必使用 BPF_F_RDONLY / BPF_F_WRONLY 标志限制Map访问权限,防止租户间数据泄露。

2.4 eBPF程序加载与交互流程

下图展示了从用户态控制平面到内核态eBPF程序的完整交互链路,以及Map在其中的桥梁作用。

eBPF程序加载与交互流程 eBPF程序加载与交互流程 用户态控制平面 eBPF加载器 控制平面代理 租户策略引擎 内核态 eBPF验证器 eBPF程序实例 eBPF Map XDP/TC/Kprobe 读/写 Map

2.5 多租户场景下的安全权限配置

在租户隔离架构中,权限配置必须遵循最小特权原则。以下是一个典型的权限配置清单:

权限配置黄金法则: 每个租户只能看到自己的Map、只能触发自己的程序、只能访问自己的数据。

2.5.1 权限配置步骤

  1. Capability剥离: 控制平面进程放弃 CAP_SYS_ADMIN,仅保留 CAP_BPF + CAP_NET_ADMIN(网络程序)或 CAP_PERFMON(跟踪程序)。
  2. Map权限掩码: 创建Map时指定 BPF_F_RDONLY(只读)或 BPF_F_WRONLY(只写),防止租户程序越权读取其他租户的统计信息。
  3. 程序类型限制: 租户只能加载与其角色匹配的程序类型(例如,网络租户只能加载XDP/TC,不能加载Kprobe)。
  4. cgroup绑定: 将eBPF程序绑定到特定cgroup,确保程序只处理属于该租户的进程/网络流量。
  5. LSM BPF增强: 使用 BPF_PROG_TYPE_LSM 钩子实现自定义的强制访问控制策略,例如禁止租户程序访问特定内核函数。

2.5.2 权限配置示例(伪代码)

// 用户态控制平面:创建租户隔离的Map
int tenant_map_fd = bpf_create_map(BPF_MAP_TYPE_HASH,
                                   sizeof(struct tenant_key),
                                   sizeof(struct tenant_value),
                                   1024,
                                   BPF_F_RDONLY | BPF_F_WRONLY);

// 加载eBPF程序并绑定到租户cgroup
struct bpf_prog_load_attr attr = {
    .prog_type = BPF_PROG_TYPE_CGROUP_SKB,
    .license = "GPL",
    .insns = prog_insns,
    .insns_cnt = prog_cnt,
};
int prog_fd = bpf_prog_load_xattr(&attr, &log_buf);

// 将程序附加到租户cgroup
int cgroup_fd = open("/sys/fs/cgroup/tenant_123", O_RDONLY);
bpf_prog_attach(prog_fd, cgroup_fd, BPF_CGROUP_INET_INGRESS, 0);
安全风险提示: 如果Map未设置权限掩码,租户A的eBPF程序可能通过 bpf_map_lookup_elem 读取租户B写入的敏感数据。务必在Map创建时明确指定 单向访问 或 per-tenant Map实例。

2.6 本章小结