本文是对 cilium/ebpf examples: fentry 的一个学习记录
在这里,我们将会利用 ebpf 监控并记录系统上所有新发起的 IPv4 TCP 连接,其工作流程如下:
- 挂载点:程序使用
fentry机制把自己附加到内核函数tcp_connect的入口。每当系统中有任何一个进程尝试发起一个 TCP 连接时,这个内核函数就会被调用,从而触发我们的 eBPF 程序。 - 过滤:程序首先检查连接的地址族是否为
AF_INET,即 IPv4。如果不是(例如是 IPv6),程序会直接退出,不做任何处理。 - 数据提取:对于 IPv4 连接,程序会从传递给
tcp_connect函数的struct sock参数中提取以下关键信息:- 源 IP 地址 (
saddr) - 目标 IP 地址 (
daddr) - 目标端口 (
dport) - 源端口 (
sport)
- 源 IP 地址 (
- 获取进程信息:使用
bpf_get_current_comm()辅助函数获取当前发起连接的进程名(例如curl,ssh等)。 - 数据发送:程序将收集到的所有信息(IP 地址、端口、进程名)打包成一个
struct event结构体,并通过一个高效的ringbuf映射发送到用户空间。
什么是 fentry
fentry 是 eBPF 中的一种程序附加类型,全称为 “function entry”(函数入口)。它是现代 Linux 内核中用于跟踪和性能分析的高效机制。
fentry 允许将 eBPF 程序附加到内核函数的入口点,当该函数被调用时,eBPF 程序会在函数的主体执行前运行。这种机制让我们可以:
- 拦截函数调用
- 检查函数参数
- 收集统计数据
- 修改执行路径
为什么使用 fentry
使用 fentry(函数入口钩子)来跟踪 TCP 连接,相比于 kprobe 或其他方式,主要有以下几个优点:
- 性能更高:fentry 是 BPF 的原生钩子,直接插入到内核函数入口,开销比 kprobe 更低,延迟更小,适合高频事件的跟踪。
- 更安全稳定:fentry 直接集成在内核 BPF 框架中,API 更加稳定,不容易因为内核升级或符号变化而失效。而 kprobe 依赖于符号解析,容易受到内核实现细节变化影响。
- 类型安全:fentry 支持 BTF(BPF Type Format),可以直接访问函数参数,类型安全且易于开发。而 kprobe 只能通过寄存器或栈手动解析参数,容易出错。
- 更好的可维护性:fentry 程序更容易与内核源码保持同步,代码更简洁,维护成本更低。
而 kprobe 适合没有 fentry 支持的老内核或特殊场景。
内核态 C 程序
| |
使用到的 bpf helper function
bpf_ringbuf_reserve()
作用:从环形缓冲区(ring buffer)中预留一块内存空间
- 参数 1:&events - 环形缓冲区的引用
- 参数 2:sizeof(struct event) - 需要预留的字节数
- 参数 3:0 - 标志位(通常为 0)
- 返回值:指向预留内存的指针,失败时返回 NULL
关键点:这是一个 " 预留 - 提交 " 模式的第一步,先申请空间但数据还未对用户空间可见。
bpf_htons()
作用:将主机字节序转换为网络字节序(Host TO Network Short)。网络协议使用大端序,而主机可能使用小端序,需要转换保证数据一致性。
- 参数:16 位的端口号
- 返回值:网络字节序的端口号
bpf_get_current_comm()
作用:获取当前进程的命令名称(进程名)
- 参数 1:&tcp_info->comm - 存储进程名的缓冲区指针
- 参数 2:TASK_COMM_LEN - 缓冲区大小(通常是 16 字节)
bpf_ringbuf_submit()
作用:将之前预留的数据提交到环形缓冲区,使其对用户空间可见
- 参数 1:tcp_info - 之前通过 bpf_ringbuf_reserve() 获得的指针
- 参数 2:0 - 标志位
用户态 Go 程序
| |