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

核心工作原理
- 事件驱动:挂载在系统调用、网络包收发、Tracepoint 或 kprobe/uprobes 等钩子上。
- 安全验证:加载前先过 Verifier 检查,避免崩溃、死循环或越界访问。
- JIT 编译:通过 JIT 将字节码转成原生指令,性能接近内核代码。
- 数据交互:通过 Map(键值存储)在内核态与用户态共享数据。
主要应用场景
- 网络加速与负载均衡:例如 Cilium 用 eBPF 做高性能容器网络和安全路由。
- 可观测性:低开销系统调用跟踪、性能剖析、链路监控,无需改应用代码。
- 运行时安全:拦截异常系统调用,实现细粒度防火墙或策略控制。
优势对比
| 特性 | eBPF 程序 | 内核模块 | 修改内核源码 |
|---|---|---|---|
| 安全性 | Verifier 保底,避免崩溃 | 代码错误可能导致死机 | 风险最高 |
| 灵活性 | 动态加载/卸载,无需重启 | 加载复杂,易引入依赖 | 需要编译并重启 |
| 性能 | JIT 后接近原生 | 高 | 高 |
如需开始开发,可参考 ebpf.io 文档,或使用 BCC、libbpf 等工具链。
eBPF动态扩展了哪些内核能力
- 深度可观测性与性能剖析 (Observability & Profiling)
- 无侵入跟踪:挂载到 kprobes/uprobes/tracepoints,实时监控函数或系统调用。
- 低成本性能监控:采集指令周期、内存、热点函数,几乎零额外开销。
- 内核态聚合:先在内核过滤/统计,只把结果送到用户态,减少数据拷贝。
- 高性能网络处理 (Networking)
- XDP 旁路:在网卡驱动层直接处理包,可做 DDoS 防护、负载均衡、防火墙。
- 容器网络:为 Kubernetes Pod 提供高效路由,绕过大量 iptables 规则。
- 自定义协议:在内核态解析非标准协议,无需回到用户态再处理。
- 动态安全控制 (Security)
- 行为审计:监控异常访问(如读取 /etc/shadow),触发告警或阻断。
- 细粒度策略:可限制特定进程只能访问某些 IP/目录。
- 流量脱敏:在内核态做 TLS 卸载或敏感字段脱敏。
- 现代内核调度与扩展 (Kernel Evolution)
- 自定义调度:借助 sched_ext,针对 AI 训练、音视频等场景写专用调度策略。
- 热补丁:无需重启即可挂载 eBPF 程序修补已知逻辑缺陷。
如何使用
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
(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";
- main.go
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
}
运行要点
- MAC、IP 与代理端口都需替换为你的实际环境;示例仅演示流程。
- bpf_sk_lookup_tcp/bpf_sk_assign 需要 CAP_BPF + CAP_NET_ADMIN 权限。
- 如果内核版本不支持 TCX,可改用
link.AttachTC作为兜底。
核心原理解析
1) 为什么需要 bpf_sk_release?
bpf_sk_lookup_tcp 在内核中查找 Socket 时,会增加该 Socket 的引用计数 (Reference Counting) 以确保在 eBPF 程序运行期间 Socket 不会被销毁。
- 必须释放:验证器 (Verifier) 会强制检查所有分支,如果获取了 Socket 指针但没有调用
bpf_sk_release,程序将无法加载。 - 归还权限:它并非关闭连接,而是告诉内核“我已用完此指针”,防止内存泄漏。
2) bpf_sk_assign 如何实现“自动转发”?
在标准流程中,内核需要根据五元组进行复杂的哈希查找来决定包给哪个 Socket。bpf_sk_assign 实现了提前绑定:
- 快速路径:它直接将 Socket 指针存入数据包结构体 (
sk_buff) 的sk字段。 - 绕过查表:当包进入传输层时,内核发现“已有主”,便跳过路由和查找逻辑,直接将包推入该 Socket 的接收队列。
- 透明代理:即使包的目标 IP 是外部服务器,通过此方法也能强行将其“截获”到本地代理程序(如 TProxy 监听器)。
树莓派环境依赖
sudo apt update
sudo apt install clang llvm libelf-dev libbpf-dev golang -y
为什么这是树莓派的最佳方案?
- CPU 负载低:在 Ingress 即完成分流,无需复杂 Netfilter 匹配。
- 内存占用小:eBPF Map 查询是常数级别的 (O(1))。
- 业务透明:终端无感知,无需装 App 或改 DNS。
注意:建议内核 5.10+。若想使用 TCX(性能更好),需要 6.6+ 内核,例如 Debian 13 (Trixie) 预览或自编译新内核。