Aya

repository·main·Indexed 26 days ago

https://github.com/aya-rs/aya

A pure Rust eBPF library focused on developer experience and operability. Aya enables writing and deploying eBPF programs without C toolchains or libbpf, utilizing BTF support for a 'compile once, run everywhere' (CO-RE) model. The ecosystem includes aya-log for eBPF logging, aya-obj for parsing ELF object files, aya-build for automating BPF binary artifacts, and aya-ebpf-macros for simplifying kprobe, uprobe, and retprobe definitions.

Tokens
15.2K
Snippets
34
Records
109
Agent score
89%

What's inside aya

  1. Overview of Aya eBPF library

    main

    Aya is a pure Rust eBPF library designed for high operability and developer experience. Unlike many other eBPF tools, it does not depend on libbpf or bcc; it uses only the libc crate to execute syscalls.

    Key features include:

    • BTF (BPF Type Format) Support: Enables CO-RE (Compile Once, Run Everywhere), allowing a single binary to run across different Linux distributions and kernel versions.
    • Advanced eBPF Features: Supports function call relocation and global data maps (global variables and initializers).
    • Async Support: Compatible with both tokio and async-std runtimes.
    • Fast Development: Does not require a C toolchain, kernel builds, or compiled headers, enabling rapid release builds.
  2. Overview of aya-obj

    main

    aya-obj is a library designed for parsing eBPF object files (ELF) compiled with libbpf or aya-bpf. It provides support for BTF (BPF Type Format) and relocations.

    Note: This crate is intended for low-level eBPF plumbing tools. If you are building standard eBPF applications, you should use the main [aya] crate instead, as aya-obj is less polished and less stable.

  3. Disable log levels at load-time to save instruction budget

    main

    Because eBPF instruction budgets are limited, you can use aya_log::LEVEL to selectively enable log levels before loading the program. This allows the verifier to prune disabled logging branches, reducing the instruction count.

    By default, all logging is enabled. You can use aya::EbpfLoader::override_global to set the level.

    • To disable all logging: Pass &0 to override_global using aya_log::LEVEL.
    • To enable specific levels: Pass the desired level as a u8 (e.g., aya_log::Level::Warn as u8).
    // Disable all logging
    let mut bpf = aya::EbpfLoader::new()
        .override_global(aya_log::LEVEL, &0, false /* must_exist */)
        .load_file("prog.bpf.o")?;
    
    // Enable only Error and Warn
    let level = aya_log::Level::Warn as u8;
    let mut bpf = EbpfLoader::new()
        .override_global(aya_log::LEVEL, &level, false /* must_exist */)
        .load_file("prog.bpf.o")?;
  4. Initialize the EbpfLogger for userspace logging

    main

    The EbpfLogger reads log records generated by aya-log-ebpf in eBPF programs and forwards them to a userspace logger (implementing the log::Log trait).

    Important: Dropping the EbpfLogger instance will close the underlying map file descriptor, which can cause subsequent eBPF program loading to fail. Ensure the logger lives as long as your eBPF program is running.

    # use aya::Ebpf;
    # // Assume bpf is a loaded Ebpf instance
    use aya_log::EbpfLogger;
    
    // Initialize env_logger as the default logger
    env_logger::init();
    
    // Start reading aya-log records and log them using the default logger
    let logger = EbpfLogger::init(&mut bpf).unwrap();
    
    // To use with async runtimes like Tokio, wrap it in an AsyncFd
    let mut logger = tokio::io::unix::AsyncFd::with_interest(logger, tokio::io::Interest::READABLE).unwrap();
    
    tokio::task::spawn(async move {
        loop {
            let mut guard = logger.readable_mut().await.unwrap();
            guard.get_inner_mut().flush();
            guard.clear_ready();
        }
    });
  5. Pin an FdLink to the BPF filesystem

    main

    You can pin an FdLink to a path on a BPF filesystem (bpffs). A pinned link remains attached even after the program that created it terminates, and will only be detached once the pinned file is removed from the filesystem. The parent directories in the provided path must already exist.

    # use aya::programs::links::FdLink;
    # use std::convert::TryInto;
    # let mut bpf = aya::Ebpf::load(&[])?;
    # let prog: &mut aya::programs::Extension = bpf.program_mut("example").unwrap().try_into()?;
    let link_id = prog.attach()?;
    let owned_link = prog.take_link(link_id)?;
    let fd_link: FdLink = owned_link.into();
    let pinned_link = fd_link.pin("/sys/fs/bpf/example")?;
  6. Use aya-log eBPF logging macros

    main

    The aya-log-ebpf package provides macros for logging within eBPF programs. These macros allow you to log structured data with different severity levels. The macros automatically handle formatting, metadata (like file, line, and module path), and efficient buffer management for eBPF environments.

    Available log levels include:

    • error!
    • warn!
    • info!
    • debug!
    • trace!

    Each macro accepts a context (e.g., ctx), an optional target (defaults to module_path!()), an optional level, a format string, and variadic arguments. When using format strings, you can provide DisplayHint hints to specify how types should be formatted (e.g., hex, IP, or MAC addresses).

  7. Define uprobe and uretprobe programs using macros

    main

    The aya-ebpf-macros package provides macros to define eBPF uprobe and uretprobe programs. These macros automatically handle the link_section naming convention required by libbpf and wrap your function to handle the raw pointer context conversion to the appropriate ProbeContext or RetProbeContext type.

    Supported Attributes

    When using the uprobe macros, you can provide the following arguments:

    • path: The file path to the target binary (e.g., path = "/usr/bin/ls").
    • function: The name of the function to probe (e.g., function = "main").
    • offset: A string representing the numeric offset (e.g., offset = "123").
    • sleepable: A boolean flag to indicate if the probe is sleepable (e.g., sleepable).
    • multi: A boolean flag to indicate if it is a multi-purpose probe (e.g., multi).

    Section Naming Convention

    The macro generates a link_section name based on your arguments following the libbpf convention:

    • Basic: uprobe or uretprobe.
    • Sleepable: uprobe.s or uretprobe.s.
    • Multi: uprobe.multi or uretprobe.multi.
    • Multi + Sleepable: uprobe.multi.s or uretprobe.multi.s.
    • With Path: uprobe/path/to/binary:function or uprobe/path/to/binary:function+offset.
  8. Use DevMap for XDP packet redirection

    main

    The DevMap is a BTF-compatible array of network devices used by XDP programs to redirect packets to other network devices.

    • Minimum Kernel Version: 4.14
    • Usage: Userspace populates slots with a target ifindex and an optional chained XDP program. The eBPF program then uses DevMap::redirect to move packets.

    To define a DevMap in your eBPF program, use the #[btf_map] macro.

    use aya_ebpf::{btf_maps::DevMap, macros::btf_map};
    
    #[btf_map]
    static DEVS: DevMap<8> = DevMap::new();
  9. Example: Using a Cgroup SKB program with Aya

    main

    This example demonstrates the workflow for loading an eBPF object file, retrieving a specific program type (BPF_PROG_TYPE_CGROUP_SKB), loading it into the kernel, and attaching it to a cgroup.

    use std::fs::File;
    use aya::Ebpf;
    use aya::programs::{CgroupSkb, CgroupSkbAttachType, CgroupAttachMode};
    
    // load the BPF code
    let mut ebpf = Ebpf::load_file("ebpf.o")?;
    
    // get the `ingress_filter` program compiled into `ebpf.o`.
    let ingress: &mut CgroupSkb = ebpf.program_mut("ingress_filter")?.try_into()?;
    
    // load the program into the kernel
    ingress.load()?;
    
    // attach the program to the root cgroup. `ingress_filter` will be called for all
    // incoming packets.
    let cgroup = File::open("/sys/fs/cgroup/unified")?;
    ingress.attach(cgroup, CgroupSkbAttachType::Ingress, CgroupAttachMode::AllowOverride)?;
  10. Parse and relocate eBPF object files with aya-obj

    main

    You can use aya-obj to parse an ELF object file and perform relocations for programs and maps. This is useful when manually loading eBPF programs into a VM like rbpf.

    1. Use Object::parse(&bytes) to load the object file.
    2. Use object.relocate_calls() to handle program call relocations.
    3. Use object.relocate_maps(iter) to handle map relocations (pass an empty iterator if no maps need relocation).
    use aya_obj::{generated::bpf_insn, Object};
    
    // Parse the object file
    let bytes = std::fs::read("program.o").unwrap();
    let mut object = Object::parse(&bytes).unwrap();
    // Relocate the programs
    object.relocate_calls().unwrap();
    object.relocate_maps(std::iter::empty()).unwrap();
    
    // Run with rbpf
    let instructions = &object.programs["prog_name"].function.instructions;
    let data = unsafe {
        core::slice::from_raw_parts(
            instructions.as_ptr() as *const u8,
            instructions.len() * core::mem::size_of::<bpf_insn>(),
        )
    };
    let vm = rbpf::EbpfVmNoData::new(Some(data)).unwrap();
    let _return = vm.execute_program().unwrap();