Rust eBPF 库 Aya 教程 - 2
在 Aya 中,Bpf
类型的实例管理通过它创建的全部 eBPF 目标的生命周期。
考虑如下程序:
use aya::Bpf;
use aya::programs::{Xdp, XdpFlags};
fn main() {
{
// 当调用 load 或 load_file 时,将创建 eBPF 代码引用的所有 Map,并且将其保存在返回的 Bpf 实例内部
let mut bpf = Bpf::load_file("bpf.o"))?;
let program: &mut Xdp = bpf.program_mut("xdp").unwrap().try_into().unwrap();
// 类似地,当将程序加载到内核时,它将存储在 Bpf 实例中
program.load()?;
// 附加程序时,它将保持附加状态,直到父 Bpf 实例被删除
program.attach("eth0", XdpFlags::default()).unwrap();
}
// 此时,bpf 变量已被销毁。程序和映射被分离/卸载
}
源代码
本章节中示例的完整源代码在这里
在编写 eBPF 程序时,经常需要使用内核在其源代码中使用的类型定义。比如当编写接收新调度的进程/任务信息的 BPF 程序时,需要 task_struct 的定义。Aya 不提供这些结构体的定义。那么如何获取这些定义呢?并且需要的是 Rust 中的定义,而非 C 中的定义。
aya-tool 为此而设计。它支持为特定的内核结构体生成 Rust 绑定。
使用如下命令进行安装:
$ cargo install bindgen-cli
$ cargo install --git https://github.com/aya-rs/aya -- aya-tool
确保系统已经安装 bpftool
和 bindgen
,否则 aya-tool 无法正常工作。
该命令的语法如下:
$ aya-tool
Usage: aya-tool <COMMAND>
Commands:
generate Generate Rust bindings to Kernel types using bpftool
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
假定生成 task_struct
的 Rust 定义,项目名称是 myapp
。用户空间部分在 myapp
子目录,eBPF 部分在 myapp-ebpf
。使用如下命令为 eBPF 部分生成该绑定:
$ aya-tool generate task_struct > myapp-ebpf/src/vmlinux.rs
为多种类型生成绑定
也可以指定多种类型,比如:
$ aya-tool generate task_struct dentry > vmlinux.rs
但是在下面的示例中,仅关注
task_struct
。
在 eBPF 程序中,使用 mod vmlinux
将 vmlinux
当作模块使用:
#![no_std]
#![no_main]
#[allow(non_upper_case_globals)]
#[allow(non_snake_case)]
#[allow(non_camel_case_types)]
#[allow(dead_code)]
mod vmlinux;
use aya_ebpf::{
cty::{c_int, c_ulong},
macros::{lsm, map},
maps::HashMap,
programs::LsmContext,
};
use vmlinux::task_struct;
#[map]
static PROCESSES: HashMap<i32, i32> = HashMap::with_max_entries(32768, 0);
#[lsm(hook = "task_alloc")]
pub fn task_alloc(ctx: LsmContext) -> i32 {
match unsafe { try_task_alloc(ctx) } {
Ok(ret) => ret,
Err(ret) => ret,
}
}
unsafe fn try_task_alloc(ctx: LsmContext) -> Result<i32, i32> {
let task: *const task_struct = ctx.arg(0);
let _clone_flags: c_ulong = ctx.arg(1);
let retval: c_int = ctx.arg(2);
// Save the PID of a new process in map.
let pid = (*task).pid;
PROCESSES.insert(&pid, &pid, 0).map_err(|e| e as i32)?;
// Handle results of previous LSM programs.
if retval != 0 {
return Ok(retval);
}
Ok(0)
}
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
unsafe { core::hint::unreachable_unchecked() }
}
由于 BPF CO-RE 机制,由 aya-tool 生成的结构体可以在不同的 Linux 内核版本之间移植。aya-tool 并非简单地从内核头文件生成这些结构体。但是目标内核(无论是哪个版本)应该开启 CONFIG_DEBUG_INFO_BTF
选项。
源代码
本节中的示例的完整代码在这里
Probe BPF 程序附加到内核(kprobe)或用户侧(uprobe)函数,可以访问这些函数的参数。可以在内核文档中查找关于 Probe 的更多信息,包括 kprobe 和 kretprobe 之间的区别。
为说明 kprobe,下面编写一个将 eBPF 处理器附加到 tcp_connect
函数的程序,该程序通过 Socket 参数打印源和目标 IP 地址。
在 BPF 程序中,使用 aya-log 打印 IP 地址,并且不适用任何 BPF 映射(除 aya-log 创建的外)。
tcp_connect
的签名可以看出 struct sock *sk
是唯一的函数参数,在程序中通过 ProbeContext
ctx 句柄访问它。bpf_probe_read_kernel
辅助函数拷贝 Socket 结构体中的 struct sock_common __sk_common
部分(对于 uprobe 程序,需要调用 bpf_probe_read_user
)。skc_family
字段,对于 AF_INET
(IPv4)和 AF_INET6
(IPv6)值,提取并且使用 aya-log info!
宏,打印源和目的 IP 地址。eBPF 代码如下:
kprobetcp-ebpf/src/main.rs
#![no_std]
#![no_main]
#[allow(non_upper_case_globals)]
#[allow(non_snake_case)]
#[allow(non_camel_case_types)]
#[allow(dead_code)]
mod binding;
use crate::binding::{sock, sock_common};
use aya_ebpf::{
helpers::bpf_probe_read_kernel, macros::kprobe, programs::ProbeContext,
};
use aya_log_ebpf::info;
const AF_INET: u16 = 2;
const AF_INET6: u16 = 10;
#[kprobe]
pub fn kprobetcp(ctx: ProbeContext) -> u32 {
match try_kprobetcp(ctx) {
Ok(ret) => ret,
Err(ret) => match ret.try_into() {
Ok(rt) => rt,
Err(_) => 1,
},
}
}
fn try_kprobetcp(ctx: ProbeContext) -> Result<u32, i64> {
let sock: *mut sock = ctx.arg(0).ok_or(1i64)?;
let sk_common = unsafe {
bpf_probe_read_kernel(&(*sock).__sk_common as *const sock_common)
.map_err(|e| e)?
};
match sk_common.skc_family {
AF_INET => {
let src_addr = u32::from_be(unsafe {
sk_common.__bindgen_anon_1.__bindgen_anon_1.skc_rcv_saddr
});
let dest_addr: u32 = u32::from_be(unsafe {
sk_common.__bindgen_anon_1.__bindgen_anon_1.skc_daddr
});
info!(
&ctx,
"AF_INET src address: {:i}, dest address: {:i}",
src_addr,
dest_addr,
);
Ok(0)
}
AF_INET6 => {
let src_addr = sk_common.skc_v6_rcv_saddr;
let dest_addr = sk_common.skc_v6_daddr;
info!(
&ctx,
"AF_INET6 src addr: {:i}, dest addr: {:i}",
unsafe { src_addr.in6_u.u6_addr8 },
unsafe { dest_addr.in6_u.u6_addr8 }
);
Ok(0)
}
_ => Ok(0),
}
}
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
unsafe { core::hint::unreachable_unchecked() }
}
用户空间代码用于加载 eBPF 程序,并且将其附加到 tcp_connect
函数。
代码如下:
use aya::{include_bytes_aligned, programs::KProbe, Bpf};
use aya_log::BpfLogger;
use clap::Parser;
use log::{info, warn};
use tokio::signal;
#[derive(Debug, Parser)]
struct Opt {}
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
let _opt = Opt::parse();
env_logger::init();
// This will include your eBPF object file as raw bytes at compile-time and load it at
// runtime. This approach is recommended for most real-world use cases. If you would
// like to specify the eBPF program at runtime rather than at compile-time, you can
// reach for `Bpf::load_file` instead.
#[cfg(debug_assertions)]
let mut bpf = Bpf::load(include_bytes_aligned!(
"../../target/bpfel-unknown-none/debug/kprobetcp"
))?;
#[cfg(not(debug_assertions))]
let mut bpf = Bpf::load(include_bytes_aligned!(
"../../target/bpfel-unknown-none/release/kprobetcp"
))?;
if let Err(e) = BpfLogger::init(&mut bpf) {
// This can happen if you remove all log statements from your eBPF program.
warn!("failed to initialize eBPF logger: {e}");
}
let program: &mut KProbe =
bpf.program_mut("kprobetcp").unwrap().try_into()?;
program.load()?;
program.attach("tcp_connect", 0)?;
info!("Waiting for Ctrl-C...");
signal::ctrl_c().await?;
info!("Exiting...");
Ok(())
}
注意:
kprobetcp/Cargo.toml
aya-tool generate sock >kprobetcp-ebpf/src/binding.rs
命令为 eBPF 程序生成内核数据结构的绑定$ RUST_LOG=info cargo xtask run --release
[2022-12-28T20:50:00Z INFO kprobetcp] Waiting for Ctrl-C...
[2022-12-28T20:50:05Z INFO kprobetcp] AF_INET6 src addr: 2001:4998:efeb:282::249, dest addr: 2606:2800:220:1:248:1893:25c8:1946
[2022-12-28T20:50:11Z INFO kprobetcp] AF_INET src address: 10.53.149.148, dest address: 10.87.116.72
[2022-12-28T20:50:30Z INFO kprobetcp] AF_INET src address: 10.53.149.148, dest address: 98.138.219.201