3. eBPF实现CNI网络策略的实时审计

任务说明

本案例目标是通过 eBPF 捕获与 CNI 网络策略执行相关的关键事件,实现策略命中与放行行为的实时审计。具体包括:

  • 跟踪容器网络命名空间内的 connect、bind、sendto 等系统调用,记录源/目的 IP、端口与进程信息;
  • 在 TC(Traffic Control)层挂钩容器虚拟网卡,捕获实际通过的数据包,标记策略放行结果;
  • 将审计事件通过 perf_event 上报用户态程序,输出结构化日志。

本案例不实现完整 CNI 插件或策略引擎,仅聚焦于“策略执行点的可观测性”这一最小闭环。

环境依赖

  • Linux 内核 ≥ 5.4(支持 BPF_PROG_TYPE_TRACEPOINT 与 BPF_PROG_TYPE_SCHED_CLS)
  • libbpf-dev、clang、llvm、gcc、make
  • 容器运行时(可选,用于验证多命名空间场景)

核心实现

以下代码包含 eBPF 内核态程序与用户态加载器,实现系统调用追踪与 TC 数据包审计。将代码保存为 audit_policy.c:

// audit_policy.c - 单文件最小实现
#include <linux/bpf.h>
#include <linux/ptrace.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/udp.h>

#define MAX_ENTRIES 1024
#define AF_INET 2

struct event_t {
    __u32 pid;
    __u32 saddr;
    __u32 daddr;
    __u16 sport;
    __u16 dport;
    __u8  proto;
    __u8  action; // 1=allow, 0=deny
    char comm[16];
};

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, __u32); // tgid
} pid_filter SEC(".maps");

SEC("tracepoint/syscalls/sys_enter_connect")
int trace_connect(struct trace_event_raw_sys_enter *ctx)
{
    __u64 pid_tgid = bpf_get_current_pid_tgid();
    __u32 pid = pid_tgid & 0xffffffff;
    __u32 tgid = pid_tgid >> 32;

    // 仅审计指定进程(由用户态注入)
    __u32 *filtered = bpf_map_lookup_elem(&pid_filter, &tgid);
    if (!filtered)
        return 0;

    struct sockaddr_in *addr = (struct sockaddr_in *)ctx->args[1];
    if (!addr || addr->sin_family != AF_INET)
        return 0;

    struct event_t ev = {};
    ev.pid = tgid;
    ev.saddr = 0; // 内核态不易获取,由用户态补充
    ev.daddr = addr->sin_addr.s_addr;
    ev.sport = 0;
    ev.dport = addr->sin_port;
    ev.proto = IPPROTO_TCP;
    ev.action = 1; // 默认放行,实际策略由用户态判定
    bpf_get_current_comm(&ev.comm, sizeof(ev.comm));

    bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &ev, sizeof(ev));
    return 0;
}

SEC("tc")
int tc_audit(struct __sk_buff *skb)
{
    void *data = (void *)(long)skb->data;
    void *data_end = (void *)(long)skb->data_end;

    struct ethhdr *eth = data;
    if ((void *)eth + sizeof(*eth) > data_end)
        return TC_ACT_OK;

    if (eth->h_proto != __bpf_constant_htons(ETH_P_IP))
        return TC_ACT_OK;

    struct iphdr *ip = (void *)(eth + 1);
    if ((void *)ip + sizeof(*ip) > data_end)
        return TC_ACT_OK;

    struct event_t ev = {};
    ev.pid = 0;
    ev.saddr = ip->saddr;
    ev.daddr = ip->daddr;
    ev.proto = ip->protocol;

    if (ip->protocol == IPPROTO_TCP) {
        struct tcphdr *tcp = (void *)(ip + 1);
        if ((void *)tcp + sizeof(*tcp) > data_end)
            return TC_ACT_OK;
        ev.sport = tcp->source;
        ev.dport = tcp->dest;
    } else if (ip->protocol == IPPROTO_UDP) {
        struct udphdr *udp = (void *)(ip + 1);
        if ((void *)udp + sizeof(*udp) > data_end)
            return TC_ACT_OK;
        ev.sport = udp->source;
        ev.dport = udp->dest;
    } else {
        return TC_ACT_OK;
    }

    ev.action = 1; // TC 层仅记录流量,策略判定由用户态完成
    bpf_perf_event_output(skb, &events, BPF_F_CURRENT_CPU, &ev, sizeof(ev));
    return TC_ACT_OK;
}

char LICENSE[] SEC("license") = "GPL";

用户态加载与事件读取

将以下代码保存为 loader.c,负责加载 eBPF 程序、注入过滤 PID、挂载 TC 钩子并打印审计事件:

// loader.c - 用户态加载器(最小实现)
#include <bpf/libbpf.h>
#include <net/if.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include "audit_policy.skel.h"

static volatile int exiting = 0;

static void handle_event(void *ctx, int cpu, void *data, __u32 size)
{
    struct event_t *ev = data;
    char saddr[32], daddr[32];
    inet_ntop(AF_INET, &ev->saddr, saddr, sizeof(saddr));
    inet_ntop(AF_INET, &ev->daddr, daddr, sizeof(daddr));
    printf("pid=%d comm=%s %s:%d -> %s:%d proto=%d action=%s\n",
           ev->pid, ev->comm, saddr, ntohs(ev->sport),
           daddr, ntohs(ev->dport), ev->proto,
           ev->action ? "ALLOW" : "DENY");
}

static void sig_handler(int sig)
{
    exiting = 1;
}

int main(int argc, char **argv)
{
    struct audit_policy_bpf *skel;
    struct bpf_program *prog;
    struct bpf_link *link;
    int ifindex;
    __u32 key, value;

    if (argc < 3) {
        fprintf(stderr, "Usage: %s <ifname> <pid_to_audit>\n", argv[0]);
        return 1;
    }

    ifindex = if_nametoindex(argv[1]);
    if (!ifindex) {
        perror("if_nametoindex");
        return 1;
    }

    skel = audit_policy_bpf__open_and_load();
    if (!skel) {
        fprintf(stderr, "Failed to load BPF skeleton\n");
        return 1;
    }

    // 注入要审计的 PID
    key = atoi(argv[2]);
    value = 1;
    bpf_map_update_elem(bpf_map__fd(skel->maps.pid_filter), &key, &value, BPF_ANY);

    // 挂载 tracepoint
    prog = skel->progs.trace_connect;
    link = bpf_program__attach(prog);
    if (!link) {
        fprintf(stderr, "Failed to attach tracepoint\n");
        goto cleanup;
    }

    // 挂载 TC ingress
    prog = skel->progs.tc_audit;
    link = bpf_program__attach_tcx(prog, ifindex, BPF_TCX_INGRESS);
    if (!link) {
        fprintf(stderr, "Failed to attach TC\n");
        goto cleanup;
    }

    signal(SIGINT, sig_handler);
    signal(SIGTERM, sig_handler);

    struct perf_buffer *pb = perf_buffer__new(
        bpf_map__fd(skel->maps.events), 8, handle_event, NULL, NULL, NULL);
    if (!pb) {
        fprintf(stderr, "Failed to create perf buffer\n");
        goto cleanup;
    }

    printf("Auditing PID %d on iface %s...\n", key, argv[1]);
    while (!exiting) {
        perf_buffer__poll(pb, 100);
    }

    perf_buffer__free(pb);
cleanup:
    audit_policy_bpf__destroy(skel);
    return 0;
}

编译与运行

# 生成 BPF 骨架头文件
bpftool gen skeleton audit_policy.c > audit_policy.skel.h

# 编译用户态程序
gcc -o audit_loader loader.c audit_policy.skel.h -lbpf -lelf -lz

# 运行(eth0 为容器网卡,1234 为要审计的进程 PID)
sudo ./audit_loader eth0 1234

验证方法

  1. 启动审计程序后,在目标 PID 对应的容器内执行 curl http://example.com 或任意 TCP 连接;
  2. 观察终端输出,应看到包含 pid=1234、目的 IP/端口及 action=ALLOW 的审计日志;
  3. 在宿主机执行 ping -c 1 8.8.8.8(非目标 PID),确认无审计输出,证明 PID 过滤生效;
  4. 通过 tc filter show dev eth0 ingress 可确认 TC 钩子已挂载。

通过条件:目标进程的网络连接产生审计日志,非目标进程不产生日志,且日志字段完整可读。