ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

eBPF可移植编程完全指南:Little Snitch for Linux 如何用CO-RE与bpf-linker跨内核版本运行

eBPF可移植编程完全指南:Little Snitch for Linux 如何用CO-RE与bpf-linker跨内核版本运行

【免费下载链接】littlesnitch-linux

Open Source components of Little Snitch for Linux

项目地址:https://gitcode.com/gh_mirrors/li/littlesnitch-linux
点击查看免费下载

Little Snitch for Linux 是一款开源(GPL-2)的 Linux 防火墙,其内核侧完全用 eBPF 编写。本文带你拆解它的eBPF 可移植编程方案:如何依靠CO-RE(Compile Once – Run Everywhere)字段重定位与bpf-linker链接器,让同一份 eBPF 程序在内核 6.12 到 7.2.0 之间免修改运行——这正是跨内核版本开发 eBPF 最关键的难题。🛡️

一、为什么 eBPF 程序必须"可移植"?

写 eBPF 程序绕不开两个现实:

  • 内核结构体布局因版本而异。要追踪"哪个进程发起了连接",程序必须读取task_struct、dentry、cred等内核结构体。这些结构体在不同内核版本、不同编译选项下,字段偏移可能完全不同,写死偏移量等于埋雷。
  • verifier 非常挑剔且会变化。内核验证器会审查每一段字节码;验证器自身、Rust 编译器或链接器一旦升级,原本能加载的程序可能被拒绝。项目 README.md 直言:"eBPF is a bit fragile",因此整条工具链必须精确固定。

Little Snitch for Linux 的解法可以概括为一句话:用"字段名"代替"偏移量",用"固定版本工具链"代替"随手升级"。

二、项目结构速览:四个组件如何分工

组件作用关键入口
ebpf/挂载到 Linux 内核的全部 eBPF 程序(基于 aya-ebpf 框架)ebpf/src/main.rs
common/内核态与用户态共享的类型与函数(流程标识、过滤引擎等)common/src/network_filter/filter_engine.rs
demo-runner/用户态示例程序:把 eBPF 程序加载进内核、加载封禁列表、消费事件队列demo-runner/src/main.rs
webroot/纯 JS 单页 Web UI(连接、封禁列表、规则三大视图)webroot/README.md

其中与"可移植性"直接相关的胶水层是 ebpf/build.rs,它在构建期完成 CO-RE 头文件的编译与绑定生成,下文细讲。

三、CO-RE 详解:用"字段名"替代硬编码偏移

3.1 声明"瘦身版"内核结构体

传统做法是 include 完整的内核头文件,但这要求编译机上的内核头与目标内核完全一致。CO-RE 的做法相反:只声明你真正用到的字段。在 ebpf/co-re/co-re.h 中,task_struct只保留了mm、pid、tgid、real_parent、cred等少数字段:

"The offset from the struct's base pointer does not matter as it will be fixed by CO-RE relocation. Just the field name is important."(偏移不重要,加载时由 CO-RE 重定位修正,字段名才是关键。)

配合 clang 的preserve_access_index属性(co-re.h#L4),编译器会为每个字段访问生成重定位指令,由加载器在运行内核上按字段名查询 BTF(类型信息)解析出真实偏移。

3.2 连"偏移计算"也要 CO-RE 化

一个容易踩的坑:即便在指针运算前算好偏移,__builtin_offsetof()的结果也可能不被 CO-RE 重定位。项目专门用__builtin_preserve_field_info()生成能穿透算术运算的BPF_CORE_FIELD_BYTE_OFFSET重定位,见 co-re.h#L144-L153:

// __builtin_offsetof() 在参与指针运算时不可靠地被重定位, // 而 __builtin_preserve_field_info() 生成的重定位可以存活 return __builtin_preserve_field_info(((struct dentry *)0)->d_name, 0);

3.3 Rust 侧如何复用这些 C 定义

ebpf/build.rs 用 bindgen 从co-re.h生成 Rust 绑定写入OUT_DIR/co-re.rs,再由 ebpf/src/main.rs#L8-L15include!进来。于是 Rust eBPF 程序可以直接像调用普通函数一样使用task_struct_uid()、linux_binprm_path()这些 CO-RE 安全访问器(见 ebpf/src/current_executable.rs#L108-L150),而实际偏移全部留到加载时解析。

四、bpf-linker 构建流程:从 Rust 到内核字节码

eBPF crate 用 Rust 编写,但内核只认一种对象格式。ebpf/build.rs 的构建流程非常值得一看:

  1. 定位 bpf-linker:通过which("bpf-linker")找到链接器,并声明为构建依赖,保证链接器升级时强制重编;
  2. 编译 C 模块:用clang --target=bpf -emit-llvm把 co-re.c 编译成 LLVM 位码(携带全部 CO-RE 重定位信息);
  3. 链接:把位码与 Rust 编译产物一起交给bpf-linker 0.10.3链接成单个 BPF 目标文件;
  4. 嵌入用户态:demo-runner 在编译期把该对象文件以原始字节嵌入二进制,运行时用 aya 框架加载。

为什么要"钉死"工具链版本?

由于 verifier 的敏感性,项目在 ebpf/rust-toolchain.toml 中固定了 eBPF 构建专用的 nightly(nightly-2026-07-07),并在 Cargo.toml#L14-L22 中把 aya 框架钉到"已验证可用"的 commit。构建脚本解析该 pin 并通过rustup run显式传递。官方口径是:这套组合在内核 6.12 到 7.2.0 上通过验证器,其他版本"may or may not work"。

五、钩子点选择:程序挂在内核的哪些位置

可移植不仅是"偏移可重定位",还包含"钩子点有 fallback"。ebpf/src/main.rs 展示了完整策略:

  • cgroup_sock / cgroup_sock_addr(Linux 4.10 / 4.17 起):在sock_create、connect4/6、sendmsg4/6拦截连接,返回SK_PASS/SK_DROP直接放行或阻断;
  • cgroup_skb(收发方向):对数据报文做流量裁决;
  • fentry/fexitbprm_execve(5.5 起)追踪进程 exec,若内核不允许 tracing,则回退到kprobe/kretprobe备选实现(main.rs#L144-L165);
  • tracepointsched_process_exec/fork/exit:维护 PID 到"可执行文件节点"的映射,构建父子进程树。

裁决事件经 RingBuf 事件队列上报用户态;demo-runner 则负责加载封禁列表(blocked_hosts.txt、blocked_domains.txt)并轮询事件,详见 demo-runner/src/main.rs#L81-L120。过滤引擎的用户态/内核态共享设计另见 common/src/network_filter/Concepts.md。

六、构建与运行:最快上手步骤

前置依赖(见 README.md#L24-L32):

rustup toolchain install stable rustup toolchain install nightly-2026-07-07 --component rust-src # 与 ebpf/rust-toolchain.toml 一致 cargo install bpf-linker --locked # 0.10.3 已知可用 # 另外需要 clang C/C++ 编译器

然后:

cargo build --release sudo cargo run --release # 以 root 运行以便挂载 cgroup 钩子

运行后 demo-runner 会打印封禁列表读取/排序/上传耗时,进入事件队列循环;按 Ctrl+C 会触发流程表、DNS 缓存、PID 缓存的 dump,方便观察 eBPF 程序内部状态。📦

七、跨内核版本避坑清单

  1. 偏移永远不要写死——一切字段访问走 CO-RE 重定位;
  2. 偏移计算函数也换掉——用__builtin_preserve_field_info()而非__builtin_offsetof()参与指针运算;
  3. 钩子点准备 fallback——fentry 不可用时退到 kprobe,tracepoint 作为最兼容的兜底;
  4. 工具链全链路钉版本——nightly Rust、bpf-linker、aya 框架 commit 三处 pin,任何一处变动都可能被 verifier 拒绝;
  5. 旧内核记得放宽 memlock——demo-runner 启动时会setrlimit(RLIMIT_MEMLOCK)(demo-runner/src/main.rs#L48-L57),老内核未启用 memcg 计账时此步不可省。

总结

Little Snitch for Linux 提供了一个教科书级的 eBPC 可移植工程范例:CO-RE 解决"结构体偏移随内核版本漂移",钉死的 nightly + bpf-linker 解决"工具链漂移导致 verifier 拒绝",钩子点 fallback 解决"不同内核配置下追踪点缺失"。三层防线叠加,才能让一份 Rust 编写的 eBPF 防火墙程序在内核 6.12 至 7.2.0 的范围内真正"编译一次、到处运行"。🚀

【免费下载链接】littlesnitch-linux

Open Source components of Little Snitch for Linux

项目地址:https://gitcode.com/gh_mirrors/li/littlesnitch-linux
点击查看免费下载

相关推荐

上一篇:Dgraph时序数据压缩策略:时间分区与压缩级别选择
下一篇:终极指南:如何通过spicetify-cli实现Spotify音乐节拍检测与歌词同步

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进