rust-bpf开发实战
安装rust工具链¶
如果安装顺利, 我们就有了cargo
和rustup
这些命令行工具, 现在安装nightly
版本
安装 Rust 工具链后, 还必须安装bpf-linker
. 链接器依赖于 LLVM, 如果您在 Linux x86_64 系统上运行,则可以根据 rust 工具链附带的版本构建链接器:
安装llvm以及clang¶
安装brftool¶
首先查看自己的内核版本
替换下面的命令为自己的内核版本
$ 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
因此我们需要在运行程序向导前提前安装:
如果出现下面的错误可能是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 ...
创建项目¶
在完成依赖后, 我们就可以使用向导来创建 eBPF 项目, 这里以 XDP 类型程序为例:
这里我们输入项目名称 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程序¶
编译完成后, 对应的程序就在targe目录下
用户空间代码¶
现在我们看一下用户控件代码的详细信息
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
验证¶
新建一个终端执行下面的命令,
这时我们运行在用户空间的程序所在控制台就开始输出日志了
eBPF 程序的生命周期¶
程序运行直到按下 CTRL+C 然后退出, 退出时, Aya 负责为我们分离程序
可以通过sudo bpftool prog list
查看是否已经 加载/分离 bpf程序
获取ip_port的bpf程序¶
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() }
}
用户程序¶
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