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
验证方法
- 启动审计程序后,在目标 PID 对应的容器内执行
curl http://example.com或任意 TCP 连接; - 观察终端输出,应看到包含
pid=1234、目的 IP/端口及action=ALLOW的审计日志; - 在宿主机执行
ping -c 1 8.8.8.8(非目标 PID),确认无审计输出,证明 PID 过滤生效; - 通过
tc filter show dev eth0 ingress可确认 TC 钩子已挂载。
通过条件:目标进程的网络连接产生审计日志,非目标进程不产生日志,且日志字段完整可读。