1. 为什么会出现ebpf
对于操作系统来说,他应该越稳定越好,100年不更新代码才好呢,但是对于运行在操作系统的软件来说,它应该变化的越快越好,比如我想新增一些设备驱动 ,或者是iptables
扩展,再或者内核到用户态进程的全链路监控(性能监控 ),再或者恶意程序在内核态挂钩关键函数(EDR杀毒 ),再或者传统的流量检测引擎都是运行在用户态的,那么赖系统调用和频繁的数据拷贝,性能开销大(CPU利用率高),且无法访问内核内部状态(如TCP重传队列)(现在有DPDK了,直接旁路协议栈更牛了)。
针对于以上场景,为了使内核更加适配软件工具,我们就需要自己去改内核代码。Man!这将非常痛苦,首先每一点改动都需要做新版本内核的适配,其次一个错误的指针操作即可导致内核Panic,影响整个服务器。因此,eBPF(扩展伯克利包过滤器)的出现,本质上是为了解决操作系统内核的"僵化"与快速变化的系统需求之间的矛盾。
2. 什么是ebpf
BPF 最初代表 Berkeley Packet Filter,但现在 eBPF(扩展 BPF)可以做的不仅仅是数据包过滤,这个首字母缩略词就不再有意义了。eBPF 现在被认为是一个独立的术语,不代表任何东西。在 Linux 源代码中,术语 BPF 仍然存在,而在工具和文档中,术语 BPF 和 eBPF 通常可以互换使用。原始 BPF 有时被称为 cBPF(经典 BPF),以区别于 eBPF。
eBPF是一项源于Linux内核的革命性技术,它能够在诸如操作系统内核这样的特权环境下运行沙盒程序。它用于在不更改内核源代码或加载内核模块的情况下安全高效地扩展内核功能。它允许沙盒程序在操作系统内运行,这意味着应用程序开发人员可以在运行时运行eBPF程序来为操作系统添加额外功能。然后,借助即时(JIT)编译器和验证引擎的帮助,操作系统保证安全性和执行效率,就像程序是原生编译的一样。这引发了一波基于eBPF的项目浪潮,涵盖了广泛的应用场景,包括下一代网络、可观测性和安全功能。
如今,eBPF被广泛用于推动各种各样的应用场景:在现代数据中心和云原生环境中提供高性能网络和负载均衡,在低开销的情况下提取细粒度的安全可观测性数据,帮助应用程序开发人员跟踪应用程序,为性能故障排除提供见解,预防性的应用程序和容器运行时安全执行,等等。
可能性是无穷无尽的,eBPF正在解锁的创新才刚刚开始。
3. ebpf是如何工作的
eBPF 程序是事件驱动的,当内核或应用程序通过某个钩子点时运行。预定义的钩子包括系统调用、函数进入/退出、内核跟踪点、网络事件和其他几个。如果不存在针对特定需求的预定义钩子,则可以创建内核探测器 (kprobe) 或用户探测器 (uprobe),以将 eBPF 程序附加到内核或用户应用程序中的几乎任何位置。
3.1. eBPF 验证器(Verifier)
功能:
确保程序不会导致内核崩溃或资源泄漏,是eBPF安全性的基石。
实现机制:
- 静态代码分析 :
对eBPF字节码进行数据流和控制流分析,禁止以下行为: -
- 未经验证的指针解引用(如:
if (data + 10 > data_end) return XDP_DROP;
) - 无限循环(通过最大指令数限制,默认1M指令)
- 非法内存访问(只能访问程序栈或Map预分配内存)
- 未经验证的指针解引用(如:
- 类型安全验证 :
基于BTF(BPF Type Format)检查内存操作的合法性,例如:
rust
struct sk_buff *skb = ctx->data_meta;
if (skb + 1 > ctx->data_end) return TC_ACT_OK; // 必须显式校验边界
- 运行时保护 :
禁止修改常量(如内核函数指针),所有分支目标必须在程序内。
3.2. BPF 虚拟机(VM)
功能:
执行eBPF字节码,提供与硬件无关的中间层。
实现机制:
- 寄存器式设计 :
11个64位通用寄存器(R0-R10),支持ALU、跳转、内存访问等操作。 - 即时编译(JIT) :
将字节码转换为本地机器码,实现接近原生性能:
bash
# 查看JIT编译后的代码
bpftool prog dump jited id <prog_id>
- 上下文感知 :
根据程序类型(XDP、kprobe等)自动注入上下文参数:
scss
SEC("kprobe/do_sys_open")
int trace_do_sys_open(struct pt_regs *ctx) {
char *filename = (void *)PT_REGS_PARM1(ctx); // 自动获取第一个参数
}
3.3. 辅助函数(Helper Functions)
功能:
提供安全的内核接口访问能力,隔离危险操作。
实现机制:
- 权限分级 :
分为基础(如bpf_map_lookup_elem
)、网络(如bpf_skb_store_bytes
)和特权级(如bpf_probe_write_user
),需CAP_BPF权限。 - 调用约定 :
通过R1-R5寄存器传参,R0保存返回值:
ini
// 示例:调用map查找辅助函数
long val = bpf_map_lookup_elem(&my_map, &key);
- 安全过滤 :
禁止直接调用任意内核函数,仅允许白名单中的辅助函数。
3.4. Map 机制
功能:
实现内核-用户态数据交换和程序间通信。
实现机制:
- 多样化数据结构:
类型 | 适用场景 |
---|---|
Hash | 键值快速查找(如连接追踪) |
LRU Hash | 自动淘汰旧条目(如缓存) |
Perf Event Array | 高性能事件上报(如指标) |
Ring Buffer | 零拷贝日志传输 |
- 原子性操作 :
通过BPF_ATOMIC_OP
指令实现并发安全:
scss
__sync_fetch_and_add(&counter, 1); // 原子递增
- 用户态接口 :
通过bpf()
系统调用或libbpf
库访问:
ini
// 用户态读取map
int fd = bpf_obj_get("/sys/fs/bpf/my_map");
bpf_map_lookup_elem(fd, &key, &value);
3.5. BTF(BPF Type Format)与 CO-RE
功能:
解决跨内核版本兼容性问题,实现"Compile Once -- Run Everywhere"。
实现机制:
- 类型元数据嵌入 :
将内核数据结构的布局信息(如结构体偏移)打包进ELF的.BTF
段:
perl
readelf -S my_prog.o | grep BTF
- 字段重定位 :
在加载时根据目标内核的BTF动态调整访问偏移:
ini
struct task_struct *task = (void *)bpf_get_current_task();
int pid = BPF_CORE_READ(task, pid); // 自动适配不同内核的pid字段位置
- CO-RE 工具链 :
bpftool gen skeleton
生成适配代码,libbpf
处理重定位细节。
3.6. 程序类型与挂钩点
功能:
定义程序执行环境和触发条件。
关键类型:
- XDP :在网卡驱动层处理数据包,支持
XDP_DROP
、XDP_TX
等动作。 - kprobe/uprobe:动态追踪内核/用户态函数。
- Tracepoint :挂载到预定义的内核跟踪点(如
sched_switch
)。 - LSM:集成Linux安全模块,实现强制访问控制。
挂钩实现:
- 动态修改指令 :
对于kprobe,通过ftrace
机制在目标函数入口插入跳转指令。 - 上下文传递 :
不同程序类型获取特定上下文结构(如XDP的xdp_md
、跟踪程序的pt_regs
)。
整体组件如下:
4. linux 环境安装ebpf
我本地是centos7.9,当我看到以下依赖的时候,大脑又嗡嗡的了,撸起袖子开搞!
To follow along with the example, you'll need:
- Linux kernel version 5.7 or later, for bpf_link support
- LLVM 11 or later 1 (
clang
andllvm-strip
) - libbpf headers 2
- Linux kernel headers 3
- Go compiler version supported by
ebpf-go
's Go module
- llvm安装
bash
wget https://github.com/llvm/llvm-project/releases/download/llvmorg-13.0.1/llvm-project-13.0.1.src.tar.xz
pwd
ls -l
tar xvf llvm-project-13.0.1.src.tar.xz
cd llvm-project-13.0.1.src
mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_RTTI=ON -DLLVM_ENABLE_PROJECTS="clang" ../llvm
- 安装不上,提示cmake版本也低
bash
cd cmake-3.14.5.tar.gz
tar zxvf cmake-3.14.5.tar.gz
yum remove cmake -y
./configure --prefix=/usr/local/cmake
- 安装不上,提示gcc版本也低
bash
yum install -y http://mirror.centos.org/centos/7/extras/x86_64/Packages/centos-release-scl-rh-2-3.el7.centos.noarch.rpm
sudo yum install -y http://mirror.centos.org/centos/7/extras/x86_64/Packages/centos-release-scl-2-3.el7.centos.noarch.rpm
vim /etc/yum.repos.d/CentOS-SCLo-scl.repo
vim /etc/yum.repos.d/CentOS-SCLo-scl-rh.repo
yum -y install devtoolset-8-gcc devtoolset-8-gcc-c++ devtoolset-8-binutils
scl enable devtoolset-8 bash
gcc --version
- 安装不上,提示python的版本也低
bash
wget https://www.python.org/ftp/python/3.8.2/Python-3.8.2.tar.xz
pwd
xz -d Python-3.8.2.tar.xz
tar xvf Python-3.8.2.tar
mkdir /usr/local/python3/
cd Python-3.8.2
./configure --prefix=/usr/local/python3
make
make -j $(nproc)
make install
ln -s /usr/local/python3/bin/python3.8 /usr/bin/python
mv /usr/bin/python /usr/bin/python.bak
mv /usr/bin/pip /usr/bin/pip.bak
ln -s /usr/local/python3/bin/python3.8 /usr/bin/python
ln -s /usr/local/python3/bin/pip3 /usr/bin/pip
python -V
- 安装了python38之后,yum还坏了
bash
vi /usr/bin/yum
vim /usr/libexec/urlgrabber-ext-down
vim /usr/bin/yum-config-manager
- libbpf安装
bash
git clone --depth 1 https://github.com/libbpf/libbpf
cd libbpf/
cd src
make install
- 安装不上,提示ldd版本太低
bash
ldd --version
wget https://mirrors.aliyun.com/gnu/glibc/glibc-2.31.tar.gz
tar -zxvf glibc-2.31.tar.gz
ls -l
cd glibc-2.31
ls -l
make -j4
ls -l
../configure --prefix=/usr --disable-profile --enable-add-ons --with-headers=/usr/include --with-binutils=/usr/bin --disable-sanity-checks --disable-werror
- 安装不上,提示make版本太低
bash
yum install bison
yum install -y kernel-devel-$(uname -r)
wget https://mirrors.aliyun.com/gnu/make/make-4.3.tar.gz
tar -zxf make-4.3.tar.gz
cd make-4.3/
mkdir build
cd build
../configure --prefix=/usr
make -j10
make install
make --version
最后整理所有依赖版本如下:
工具 | 版本 | |
---|---|---|
python | 3.8 | |
make | GNU Make 4.3 | |
bison | bison (GNU Bison) 3.0.4 | |
gcc | gcc (GCC) 8.3.1 20190311 | |
cmake | version 3.14.5 | |
ldd | ldd (GNU libc) 2.31 | |
llvm | clang version 13.0.1 | |
libbpf | v3.10.0-1160.119.1.el7.x86_64.debug | |
5. smac修改的小demo
eBPF-Go 是由 Cilium 团队开发的一个 Go 语言库,用于在用户空间与 Linux 内核的 eBPF(扩展伯克利包过滤器)功能交互。它提供了一套高级 API,使开发者能够用 Go 编写用户态程序,管理 eBPF 字节码的加载、事件处理以及与内核态 eBPF 程序的数据交换。
这次demo就采用大佬团队的eBPF-Go,然后hook在xdp位置,在网卡收到报文的时候就对它的source mac进行修改,大致位置如下:
也就是说,预期效果应该是用tcpdump抓到的报文,显示的源mac就是我们修改的mac()。
代码如下:
c
//go:build ignore
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
SEC("xdp")
int xdp_rewrite_mac(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
if (data + sizeof(*eth) > data_end)
return XDP_PASS;
unsigned char new_smac[ETH_ALEN] = {0x1, 0x2, 0x3, 0x4, 0x5, 0x6};
__builtin_memcpy(eth->h_source, new_smac, ETH_ALEN);
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
package main
import (
"flag"
"log"
"net"
"os"
"os/signal"
"time"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/rlimit"
)
func main() {
// Remove resource limits for kernels <5.11.
if err := rlimit.RemoveMemlock(); err != nil {
log.Fatal("Removing memlock:", err)
}
// Load the compiled eBPF ELF and load it into the kernel.
var objs counterObjects
if err := loadCounterObjects(&objs, nil); err != nil {
log.Fatal("Loading eBPF objects:", err)
}
defer objs.Close()
ifname := flag.String("ifname", "ifname", "监听的网卡名")
flag.Parse()
iface, err := net.InterfaceByName(*ifname)
if err != nil {
log.Fatalf("Getting interface %s: %s", ifname, err)
}
// Attach count_packets to the network interface.
link, err := link.AttachXDP(link.XDPOptions{
Program: objs.XdpRewriteMac,
Interface: iface.Index,
})
if err != nil {
log.Fatal("Attaching XDP:", err)
}
defer link.Close()
//log.Printf("Counting incoming packets on %s..", ifname)
// Periodically fetch the packet counter from PktCount,
// exit the program when interrupted.
tick := time.Tick(time.Second)
stop := make(chan os.Signal, 5)
signal.Notify(stop, os.Interrupt)
for {
select {
case <-tick:
// var count uint64
// err := objs.PktCount.Lookup(uint32(0), &count)
// if err != nil {
// log.Fatal("Map lookup:", err)
// }
//log.Printf("Received %d packets", count)
case <-stop:
log.Print("Received signal, exiting..")
return
}
}
}
大功告成,溜了