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

运行与验证

  1. 将上述C代码保存为 netns_monitor.c,Python代码保存为 monitor.py。
  2. 安装BCC工具集:apt install bpfcc-tools python3-bcc(Ubuntu)或 yum install bcc-tools python3-bcc(CentOS)。
  3. 以root权限运行:sudo python3 monitor.py。
  4. 在另一个终端创建容器:docker run --rm -it alpine sh(或使用CNI插件手动创建网络命名空间)。
  5. 观察监控输出,应看到类似: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 调用,需额外挂载对应函数。