跳转至

rust-bpf开发实战

仓库地址: https://git.coder.rs/yanguangshaonian/bpf_stu

安装rust工具链

$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

如果安装顺利, 我们就有了cargorustup这些命令行工具, 现在安装nightly版本

$ rustup toolchain install nightly --component rust-src

安装 Rust 工具链后, 还必须安装bpf-linker. 链接器依赖于 LLVM, 如果您在 Linux x86_64 系统上运行,则可以根据 rust 工具链附带的版本构建链接器:

$ cargo install bpf-linker

安装llvm以及clang

$ sudo apt-get update
$ sudo apt-get install llvm clang -y

安装brftool

首先查看自己的内核版本

$ uname -r

替换下面的命令为自己的内核版本

$ sudo apt install linux-tools-common linux-tools-5.15.0-52-generic linux-cloud-tools-5.15.0-52-generic -y

安装aya提供的脚手架

Aya 提供了一套模版向导用于创建 eBPF 对应的程序类型, 向导创建依赖于cargo-generate因此我们需要在运行程序向导前提前安装:

$ cargo install cargo-generate

如果出现下面的错误可能是openssl库导致的, 修复后, 重新执行上面的命令安装即可

... warning: build failed, waiting for other jobs to finish... error: failed to compile cargo-generate v0.16.0, intermediate artifacts can be found at `/tmp/cargo-install8NrREg ...

$ sudo apt install openssl pkg-config libssl-dev gcc m4 ca-certificates make perl -y

创建项目

在完成依赖后, 我们就可以使用向导来创建 eBPF 项目, 这里以 XDP 类型程序为例:

$ cargo generate https://github.com/aya-rs/aya-template

这里我们输入项目名称 myapp, eBPF 程序类型选择 xdp, 完成相关设定后, 向导会自动帮我们创建一个名为 myapp 的 Rust 项目, 项目包括了一个最简单的 XDP 类型的 eBPF 程序及相对应的用户空间程序, myapp 目录的整体夹头如下所示

├── Cargo.lock
├── Cargo.toml
├── README.md
├── myapp  # 用户空间程序
   ├── Cargo.toml
   └── src
       └── main.rs
├── myapp-common  # eBPF 程序与用户空间程序复用的代码库
   ├── Cargo.toml
   └── src
       └── lib.rs
├── myapp-ebpf  # eBPF 程序
   ├── Cargo.lock
   ├── Cargo.toml
   ├── rust-toolchain.toml
   └── src
       └── main.rs
└── xtask  # build 相关的代码
    ├── Cargo.toml
    └── src
        ├── build_ebpf.rs
        ├── main.rs
        └── run.rs

8 directories, 15 files

ebpf程序

生成的 eBPF 程序位于 myapp-ebpf/src 目录下, 文件名为`main.rs, 完整内容如下所示

#![no_std]
#![no_main]

use aya_bpf::{bindings::xdp_action, macros::xdp, programs::XdpContext};
use aya_log_ebpf::info;

#[xdp(name = "myapp")]  // 这表明该函数是一个XDP程序, 我们自定义了一个名字
pub fn myapp(ctx: XdpContext) -> u32 {
    // 执行另一个函数验证之后, 返回结果, 如果出错, 则丢弃数据包
    match try_myapp(ctx) {
        Ok(ret) => ret,
        Err(_) => xdp_action::XDP_ABORTED,  // 丢弃数据包
    }
}

fn try_myapp(ctx: XdpContext) -> Result<u32, u32> {
    info!(&ctx, "received a packet");  // 每次接收到数据包, 就输出一个日志
    Ok(xdp_action::XDP_PASS)  // 允许通过所有流量
}

// 虽然我们不能发生恐慌, 但是因为我们没std和main 所以这里需要我们指定一个panic发生时的处理函数
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    unsafe { core::hint::unreachable_unchecked() }
}

编译ebpf程序

$ cargo xtask build-ebpf

编译完成后, 对应的程序就在targe目录下

$ llvm-objdump -S target/bpfel-unknown-none/debug/myapp

用户空间代码

现在我们看一下用户控件代码的详细信息

use anyhow::Context;
use aya::programs::{Xdp, XdpFlags};
use aya::{include_bytes_aligned, Bpf};
use aya_log::BpfLogger;
use clap::Parser;
use log::{info, warn};
use tokio::signal;

#[derive(Debug, Parser)]
struct Opt {
    #[clap(short, long, default_value = "eth0")]
    iface: String,
}

#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
    let opt = Opt::parse();

    env_logger::init();

    // include_bytes_aligned 这个宏 编译时以二进制数据加载到当前程序, 推荐这个, 如果想在运行时而不是编译时指定eBPF程序, 则使用Bpf::load_file,
    // load 会解析bpf二进制数据相关的段, 并加载内核缓冲中
    #[cfg(debug_assertions)]
    let mut bpf = Bpf::load(include_bytes_aligned!(
        "../../target/bpfel-unknown-none/debug/myapp"
    ))?;
    #[cfg(not(debug_assertions))]
    let mut bpf = Bpf::load(include_bytes_aligned!(
        "../../target/bpfel-unknown-none/release/myapp"
    ))?;

    // 初始化日志
    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);
    }

    // 我们从 bpf二进制 应用中找到程序
    // program_mut 用于在加载和附加程序之前获取程序
    let program: &mut Xdp = bpf.program_mut("myapp").unwrap().try_into()?;
    // 将程序加载到内核中
    program.load()?;

    // 将程序在内核中附加到指定位置
    program.attach(&opt.iface, XdpFlags::default())
        .context("failed to attach the XDP program with default flags - try changing XdpFlags::default() to XdpFlags::SKB_MODE")?;

    info!("Waiting for Ctrl-C...");
    signal::ctrl_c().await?;
    info!("Exiting...");
    // 程序结束, 执行drop, bpf生命周期结束
    Ok(())
}

编译用户空间代码

执行RUST_LOG=info cargo xtask run 会出现下面的错误

...
    Finished dev [unoptimized + debuginfo] target(s) in 8.38s
Error: failed to attach the XDP program with default flags - try changing XdpFlags::default() to XdpFlags::SKB_MODE

Caused by:
    unknown network interface eth0

因为RUST_LOG=info 为设置日志级别的环境变量, 默认为 warn, 但向导生成的代码打印的日志级别默认为 info, 因此需要运行时制定, 否则可能会出现程序运行查看不到日志的情况

cargo xtask run 命令会直接编译用户空间代码并运行, 是运行过程中我们发现出现错误 unknown network interface eth0, 这是因为默认生成的程序指定将 XDP 程序加载到 eth0 网卡, 而我们的 VM 默认网卡不为 eth0 导致, 这里我们明确网卡使用 lo 测试, 再次运行结果如下

$ RUST_LOG=info cargo xtask run -- --iface lo
...
    Finished dev [optimized] target(s) in 0.19s
    Finished dev [unoptimized + debuginfo] target(s) in 0.12s
[2022-11-05T16:25:27Z INFO  myapp] Waiting for Ctrl-C...

这次可以发现用户空间程序已经正常运行, 并且将对应的 eBPF 程序加载至内核中

$ sudo bpftool prog list
42: xdp  name myapp  tag 2929f83b3be0f64b  gpl
    loaded_at 2022-11-06T22:42:54+0800  uid 0
    xlated 2016B  jited 1151B  memlock 4096B  map_ids 14,13,15

$ ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 xdpgeneric qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    prog/xdp id 42 # <=== 加载的 eBPF 程序 id 42

验证

新建一个终端执行下面的命令,

ping -c 1 127.0.0.1

这时我们运行在用户空间的程序所在控制台就开始输出日志了

eBPF 程序的生命周期

程序运行直到按下 CTRL+C 然后退出, 退出时, Aya 负责为我们分离程序

可以通过sudo bpftool prog list查看是否已经 加载/分离 bpf程序

获取ip_port的bpf程序

ebpf程序

$ cargo xtask build-ebpf
#![no_std]
#![no_main]

use aya_bpf::{bindings::xdp_action, macros::xdp, programs::XdpContext};
use aya_log_ebpf::info;

use core::mem;
use network_types::{
    eth::{EthHdr, EtherType},
    ip::{IpProto, Ipv4Hdr},
    tcp::TcpHdr,
    udp::UdpHdr,
};

#[xdp]
pub fn xdp_firewall(ctx: XdpContext) -> u32 {
    match try_xdp_firewall(ctx) {
        Ok(ret) => ret,
        Err(_) => xdp_action::XDP_ABORTED,
    }
}

#[inline(always)]  // 建议编译器内联此函数, 这个函数负责吧xdp中的数据, 指定偏移位置转为T指针
fn ptr_at<T>(ctx: &XdpContext, offset: usize) -> Result<*const T, ()> {
    // 得到ctx中数据的起始位置
    let start = ctx.data();
    // 得到ctx中数据的结束位置
    let end = ctx.data_end();
    // 得到需要类型T的大小
    let len = mem::size_of::<T>();

    // 如果 起始位置 + T的偏移位置 + T的大小 超过了结束位置, 说明越界了, 返回错误
    if start + offset + len > end {
        return Err(());
    }

    // 否则把 起始位置 + T的偏移位置 的开头强转为 T
    Ok((start + offset) as *const T)
}

fn try_xdp_firewall(ctx: XdpContext) -> Result<u32, ()> {
    let ethhdr: *const EthHdr = ptr_at(&ctx, 0)?; // 得到 EthHdr 以太网报头结构体的指针, 他在 0 offset 的位置
    // 过滤协议, 我们只处理 ipv4 的地址, 如果不是 直接让其通过
    match unsafe { (*ethhdr).ether_type } {
        EtherType::Ipv4 => {}
        _ => return Ok(xdp_action::XDP_PASS),
    }

    // ipv4 相关信息 在以太网报头的后面
    let ipv4hdr: *const Ipv4Hdr = ptr_at(&ctx, EthHdr::LEN)?;
    // 将大端字节序 Big-Endian 的字节序列转换为 u32 类型的函数, 得到 src ip
    let source_addr = u32::from_be(unsafe { (*ipv4hdr).src_addr });

    // 根据传输协议(在 ip 报头的后面), 得到 port
    let source_port = match unsafe { (*ipv4hdr).proto } {
        IpProto::Tcp => {
            let tcphdr: *const TcpHdr =
                ptr_at(&ctx, EthHdr::LEN + Ipv4Hdr::LEN)?;
            u16::from_be(unsafe { (*tcphdr).source })
        }
        IpProto::Udp => {
            let udphdr: *const UdpHdr =
                ptr_at(&ctx, EthHdr::LEN + Ipv4Hdr::LEN)?;
            u16::from_be(unsafe { (*udphdr).source })
        }
        _ => return Err(()),
    };

    // 
    info!(
        &ctx,
        "SRC IP: {}, SRC PORT: {}", source_addr, source_port
    );

    Ok(xdp_action::XDP_PASS)
}

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    unsafe { core::hint::unreachable_unchecked() }
}

用户程序

$ cargo build
use aya::{include_bytes_aligned, Bpf};
use anyhow::Context;
use aya::programs::{Xdp, XdpFlags};
use aya_log::BpfLogger;
use clap::Parser;
use log::{info, warn};
use tokio::signal;

#[derive(Debug, Parser)]
struct Opt {
    #[clap(short, long, default_value = "eth0")]
    iface: String,
}

#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
    let opt = Opt::parse();

    env_logger::init();

    #[cfg(debug_assertions)]
        let mut bpf = Bpf::load(include_bytes_aligned!(
        "../../target/bpfel-unknown-none/debug/parse_xdp"
    ))?;
    #[cfg(not(debug_assertions))]
        let mut bpf = Bpf::load(include_bytes_aligned!(
        "../../target/bpfel-unknown-none/release/parse_xdp"
    ))?;
    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 Xdp = bpf.program_mut("xdp").unwrap().try_into()?;
    program.load()?;
    program.attach(&opt.iface, XdpFlags::default())
        .context("failed to attach the XDP program with default flags - try changing XdpFlags::default() to XdpFlags::SKB_MODE")?;

    info!("Waiting for Ctrl-C...");
    signal::ctrl_c().await?;
    info!("Exiting...");

    Ok(())
}

运行

记得加上sudo

$ sudo RUST_LOG=info target/debug/parse_xdp --iface lo