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操作生命周期
- 创建(用户态): 通过
bpf(BPF_MAP_CREATE, ...)系统调用指定类型、键值大小、最大条目数。 - 加载(用户态→内核): eBPF程序通过
SEC(".maps")引用Map,加载器自动关联。 - 访问(内核态): 程序内使用
bpf_map_lookup_elem()、bpf_map_update_elem()等辅助函数。 - 控制(用户态): 控制平面通过
bpf(BPF_MAP_LOOKUP_ELEM, ...)读取或更新Map内容。 - 销毁(用户态): 关闭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在其中的桥梁作用。
2.5 多租户场景下的安全权限配置
在租户隔离架构中,权限配置必须遵循最小特权原则。以下是一个典型的权限配置清单:
权限配置黄金法则: 每个租户只能看到自己的Map、只能触发自己的程序、只能访问自己的数据。
2.5.1 权限配置步骤
- Capability剥离: 控制平面进程放弃
CAP_SYS_ADMIN,仅保留CAP_BPF+CAP_NET_ADMIN(网络程序)或CAP_PERFMON(跟踪程序)。 - Map权限掩码: 创建Map时指定
BPF_F_RDONLY(只读)或BPF_F_WRONLY(只写),防止租户程序越权读取其他租户的统计信息。 - 程序类型限制: 租户只能加载与其角色匹配的程序类型(例如,网络租户只能加载XDP/TC,不能加载Kprobe)。
- cgroup绑定: 将eBPF程序绑定到特定cgroup,确保程序只处理属于该租户的进程/网络流量。
- 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实例。