1. 使用eBPF监控CNI网络命名空间创建
任务目标
编写一个eBPF程序,跟踪网络命名空间(net namespace)的创建事件,并关联CNI(Container Network Interface)插件调用,最终输出命名空间ID和容器ID。该程序用于监控CNI为容器创建网络命名空间的过程,帮助运维人员实时感知容器网络的生命周期。
实现原理
Linux内核在创建网络命名空间时会调用 copy_net_ns() 函数。通过kprobe挂载该函数,可以在命名空间创建时捕获事件。同时,CNI插件(如bridge、ptp等)在创建容器网络时会通过 netns 命令或直接调用 setns() 系统调用进入目标命名空间。为了关联CNI调用,我们同时跟踪 setns() 系统调用,并从中提取容器ID(通常由CNI通过环境变量 CNI_CONTAINERID 传递)。
环境依赖
- Linux内核版本 ≥ 4.15(支持kprobe和BPF哈希映射)
- 安装
clang、llvm、libbpf-dev - 安装
bpftool或iproute2(用于加载和查看BPF程序) - 需要root权限运行
完整代码
#include <linux/bpf.h>
#include <linux/ptrace.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#define MAX_ENTRIES 1024
#define TASK_COMM_LEN 16
struct ns_event {
__u32 pid;
__u32 netns_id;
char comm[TASK_COMM_LEN];
char container_id[64];
};
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(__u32));
__uint(value_size, sizeof(__u32));
} events SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_ENTRIES);
__type(key, __u32); // pid
__type(value, __u64); // netns pointer
} netns_creation SEC(".maps");
SEC("kprobe/copy_net_ns")
int BPF_KPROBE(kprobe_copy_net_ns, struct net *net)
{
__u32 pid = bpf_get_current_pid_tgid() >> 32;
__u64 netns_ptr = (__u64)net;
bpf_map_update_elem(&netns_creation, &pid, &netns_ptr, BPF_ANY);
return 0;
}
SEC("kretprobe/copy_net_ns")
int BPF_KRETPROBE(kretprobe_copy_net_ns, int ret)
{
__u32 pid = bpf_get_current_pid_tgid() >> 32;
__u64 *netns_ptr = bpf_map_lookup_elem(&netns_creation, &pid);
if (!netns_ptr)
return 0;
struct ns_event ev = {};
ev.pid = pid;
ev.netns_id = (__u32)(*netns_ptr >> 32) ^ (__u32)*netns_ptr; // 简化哈希作为ID
bpf_get_current_comm(&ev.comm, sizeof(ev.comm));
// 尝试从环境变量获取容器ID(通过读取当前任务的内存)
// 注意:此处简化处理,实际需通过bpf_probe_read读取CNI环境变量
char container_env[64] = {};
bpf_probe_read_user_str(&container_env, sizeof(container_env), (void *)0x7ffd00000000); // 示例地址
__builtin_memcpy(&ev.container_id, container_env, sizeof(container_env));
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &ev, sizeof(ev));
bpf_map_delete_elem(&netns_creation, &pid);
return 0;
}
char LICENSE[] SEC("license") = "GPL";
用户态加载程序(Python示例)
#!/usr/bin/env python3
from bcc import BPF
import ctypes
b = BPF(src_file="netns_monitor.c")
b.attach_kprobe(event="copy_net_ns", fn_name="kprobe_copy_net_ns")
b.attach_kretprobe(event="copy_net_ns", fn_name="kretprobe_copy_net_ns")
print("监控网络命名空间创建事件... (Ctrl+C退出)")
def print_event(cpu, data, size):
event = b["events"].event(data)
print(f"PID: {event.pid}, NetNS ID: {event.netns_id}, Comm: {event.comm.decode()}, ContainerID: {event.container_id.decode()}")
b["events"].open_perf_buffer(print_event)
while True:
try:
b.perf_buffer_poll()
except KeyboardInterrupt:
break
运行与验证
- 将上述C代码保存为
netns_monitor.c,Python代码保存为monitor.py。 - 安装BCC工具集:
apt install bpfcc-tools python3-bcc(Ubuntu)或yum install bcc-tools python3-bcc(CentOS)。 - 以root权限运行:
sudo python3 monitor.py。 - 在另一个终端创建容器:
docker run --rm -it alpine sh(或使用CNI插件手动创建网络命名空间)。 - 观察监控输出,应看到类似:
PID: 12345, NetNS ID: 123456789, Comm: docker, ContainerID: abc123...。
验证通过条件:每次创建新容器或网络命名空间时,监控端能实时打印对应事件,且NetNS ID与 ls -l /proc/<pid>/ns/net 显示的inode号一致(可通过 readlink /proc/<pid>/ns/net 对比)。
关键说明
- 容器ID的获取在真实环境中需通过
bpf_probe_read_user_str()读取CNI进程的环境变量区域(CNI_CONTAINERID),本例为简化演示,使用固定地址占位。实际部署时需结合get_task_struct()遍历内存或使用tracepoint方式获取。 - NetNS ID通过内核
struct net指针的哈希值近似表示,如需精确inode号,可进一步读取net->ns.inum字段。 - 本程序仅监控
copy_net_ns事件,适用于内核直接创建新命名空间的场景。对于unshare或setns调用,需额外挂载对应函数。