Rust eBPF 库 Aya 教程 - 2


1. 使用 Aya

1.1. 程序生命周期

在 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 变量已被销毁。程序和映射被分离/卸载

}

1.2. 使用 aya-tool

源代码

本章节中示例的完整源代码在这里

在编写 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

确保系统已经安装 bpftoolbindgen,否则 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 vmlinuxvmlinux 当作模块使用:

#![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() }
}

1.2.1. 可移植性以及不同内核版本

由于 BPF CO-RE 机制,由 aya-tool 生成的结构体可以在不同的 Linux 内核版本之间移植。aya-tool 并非简单地从内核头文件生成这些结构体。但是目标内核(无论是哪个版本)应该开启 CONFIG_DEBUG_INFO_BTF 选项。


2. 程序类型 - Probe

源代码

本节中的示例的完整代码在这里

2.1. eBPF Probe 是什么?

Probe BPF 程序附加到内核(kprobe)或用户侧(uprobe)函数,可以访问这些函数的参数。可以在内核文档中查找关于 Probe 的更多信息,包括 kprobe 和 kretprobe 之间的区别。

2.2. 示例项目

为说明 kprobe,下面编写一个将 eBPF 处理器附加到 tcp_connect 函数的程序,该程序通过 Socket 参数打印源和目标 IP 地址。

2.3 设计

在 BPF 程序中,使用 aya-log 打印 IP 地址,并且不适用任何 BPF 映射(除 aya-log 创建的外)。

2.4. eBPF 代码

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() }
}

2.5. 用户空间代码

用户空间代码用于加载 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(())
}

注意:

  1. 按照完整源代码修改 kprobetcp/Cargo.toml
  2. 在项目目录下执行 aya-tool generate sock >kprobetcp-ebpf/src/binding.rs 命令为 eBPF 程序生成内核数据结构的绑定

2.6. 运行程序

$ 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

环境说明