eBPF介绍

eBPF(Extended Berkeley Packet Filter)允许开发者在不修改内核源码、不加载内核模块的情况下,把小段受控程序安全地运行在内核里。它通过沙盒虚拟机让内核变得可编程,广泛用于网络、安全、可观测性等场景。

What is eBPF

核心工作原理

主要应用场景

优势对比

特性eBPF 程序内核模块修改内核源码
安全性Verifier 保底,避免崩溃代码错误可能导致死机风险最高
灵活性动态加载/卸载,无需重启加载复杂,易引入依赖需要编译并重启
性能JIT 后接近原生

如需开始开发,可参考 ebpf.io 文档,或使用 BCC、libbpf 等工具链。

eBPF动态扩展了哪些内核能力

  1. 深度可观测性与性能剖析 (Observability & Profiling)
  1. 高性能网络处理 (Networking)
  1. 动态安全控制 (Security)
  1. 现代内核调度与扩展 (Kernel Evolution)

如何使用

eBPF工作流程

graph TD subgraph User_Space ["用户态 (User Space)"] A["编写 eBPF 源代码
(C / Rust)"] -- "LLVM / Clang" --> B["eBPF 字节码
(Bytecode)"] B -- "bpf 系统调用" --> C["加载并注入内核"] M["用户态控制程序
Go / C++ / Rust"] M -- "分析数据" --> N["可视化 / 控制面板"] end subgraph Kernel_Space ["内核态 (Kernel Space)"] C --> D{"验证器 Verifier
安全/权限检查"} D -- "拒绝" --> E["报错并停止加载"] D -- "通过" --> F["JIT 即时编译器"] F --> G["原生机器码
Native Machine Code"] subgraph Hooks ["事件钩子 (Event Hooks)"] H["网络包 XDP/TC"] I["系统调用 Syscalls"] J["内核函数 Kprobes"] K["用户函数 Uprobes"] end G -- "挂载执行" --> H G -- "挂载执行" --> I G -- "挂载执行" --> J G -- "挂载执行" --> K L["eBPF Maps
共享存储/键值对"] H & I & J & K -- "数据交互" --> L end L -- "bpf 系统调用
(读写数据)" --- M style D fill:#f96,stroke:#333,stroke-width:2px style G fill:#00d2ff,stroke:#333,stroke-width:2px style L fill:#fff4dd,stroke:#d4a017,stroke-width:2px

一个加速小例子

场景:为局域网内的平板/手机做游戏加速。思路是:数据包进入网关后,如果源 MAC 在“加速名单”且目标 IP 在“游戏服务器列表”,则把流量指派给本地 TProxy 代理端口(示例中用 10080),否则直连。

为了方便开发,使用 Go 版本的 Cilium eBPF SDK。

// accelerator.c
// +build ignore
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/in.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>

// 存储需要加速的设备 MAC
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 128);
    __type(key, unsigned char[6]);
    __type(value, __u32);
} acc_macs SEC(".maps");

// 存储游戏服务器 IP
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 1024);
    __type(key, __u32); // 目标 IP
    __type(value, __u32);
} game_ips SEC(".maps");

SEC("tc")
int fast_game_assign(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 + 1) > data_end) return TC_ACT_OK;

    // 1. 过滤 MAC 地址
    if (!bpf_map_lookup_elem(&acc_macs, eth->h_source)) {
        return TC_ACT_OK;
    }

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

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

    // 仅处理 TCP 流量
    if (ip->protocol != IPPROTO_TCP) return TC_ACT_OK;

    // 2. 过滤游戏服务器 IP
    __u32 dest_ip = ip->daddr;
    if (bpf_map_lookup_elem(&game_ips, &dest_ip)) {
        struct tcphdr *tcp = (void *)(ip + 1);
        if ((void *)(tcp + 1) > data_end) return TC_ACT_OK;

        // 3. 核心:查找本地监听 10080 端口的 TProxy Socket
        // 构造四元组,尝试在本地网络命名空间寻找对应的监听器
        struct bpf_sock_tuple tuple = {};
        tuple.ipv4.daddr = ip->daddr;
        tuple.ipv4.dport = bpf_htons(10080); // 目标指向本地代理端口
        tuple.ipv4.saddr = ip->saddr;
        tuple.ipv4.sport = tcp->source;
        
        // 查找 TCP 监听 Socket
        struct bpf_sock *sk = bpf_sk_lookup_tcp(skb, &tuple, sizeof(tuple.ipv4), BPF_F_NETNS, 0);
        if (sk) {
            // 将数据包指派给该 Socket,实现透明代理转发
            if (bpf_sk_assign(skb, sk, 0) == 0) {
                bpf_sk_release(sk);
                return TC_ACT_OK; 
            }
            bpf_sk_release(sk);
        }
    }

    return TC_ACT_OK;
}

char _license[] SEC("license") = "GPL";
package main

import (
	"log"
	"net"
	"os"
	"os/signal"
	"syscall"

    "github.com/cilium/ebpf"
    "github.com/cilium/ebpf/link"
    "github.com/cilium/ebpf/rlimit"
)

// 使用 bpf2go 自动生成 Go 代码
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target bpf bpf accelerator.c

func main() {
	// 1. 移除内核内存限制(旧内核需要)
	if err := rlimit.RemoveMemlock(); err != nil {
		log.Fatal(err)
	}

	// 2. 加载编译好的 eBPF 对象
	objs := bpfObjects{}
	if err := loadBpfObjects(&objs, nil); err != nil {
		log.Fatalf("loading objects: %v", err)
	}
	defer objs.Close()

	// 3. 设置加速名单
	// 手机 MAC 地址
	mac, _ := net.ParseMAC("AA:BB:CC:DD:EE:FF")
	var macKey [6]byte
	copy(macKey[:], mac)
	objs.AccMacs.Put(macKey, uint32(1))

	// 游戏服务器 IP (例如 1.2.3.4)
	gameIP := net.ParseIP("1.2.3.4").To4()
	objs.GameIps.Put(gameIP, uint32(1))

	// 4. 获取网卡接口 (树莓派通常是 eth0 或 wlan0)
	iface, err := net.InterfaceByName("eth0")
	if err != nil {
		log.Fatalf("lookup network iface: %v", err)
	}

	// 5. 挂载到 TCX 钩子 (Linux 6.6+ 最佳性能)
	l, err := link.AttachTCX(link.TCXOptions{
		Interface: iface.Index,
		Program:   objs.FastGameAssign,
		Attach:    ebpf.AttachTCXIngress,
	})
	if err != nil {
		// 如果内核版本较低不支持 TCX,则回退到传统 TC
		log.Fatalf("failed to attach TCX: %v", err)
	}
	defer l.Close()

	log.Printf("树莓派加速器启动成功!已挂载至 %s\n", iface.Name)
	log.Println("监听 MAC: AA:BB:CC:DD:EE:FF -> 游戏服务器 IP: 1.2.3.4")

	// 6. 等待退出信号
	stop := make(chan os.Signal, 1)
	signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
	<-stop
}

运行要点

核心原理解析

1) 为什么需要 bpf_sk_release

bpf_sk_lookup_tcp 在内核中查找 Socket 时,会增加该 Socket 的引用计数 (Reference Counting) 以确保在 eBPF 程序运行期间 Socket 不会被销毁。

2) bpf_sk_assign 如何实现“自动转发”?

在标准流程中,内核需要根据五元组进行复杂的哈希查找来决定包给哪个 Socket。bpf_sk_assign 实现了提前绑定

树莓派环境依赖

sudo apt update
sudo apt install clang llvm libelf-dev libbpf-dev golang -y

为什么这是树莓派的最佳方案?

注意:建议内核 5.10+。若想使用 TCX(性能更好),需要 6.6+ 内核,例如 Debian 13 (Trixie) 预览或自编译新内核。

参考