用Rust玩eBPF —— aya-rs

环境搭建

笔者这里是 Ubuntu 22.04 。

rust环境

1
2
rustup install stable
rustup toolchain install nightly --component rust-src

bpflinker

安装bpflinker

1
cargo install bpf-linker

安装cargo generate

用于生成模板项目。

1
cargo install cargo-generate

生成模板项目

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

如果和我一样配置了git的ssh代理,那可能会不幸遇到这样的错误

1
2
3
4
5
⚠️   Favorite `https://github.com/aya-rs/aya-template` not found in config, using it as a git repository: https://github.com/aya-rs/aya-template
Error: Please check if the Git user / repository exists.

Caused by:
unknown http scheme 'socks5'; class=Http (34)

看起来cargo generate并不支持这种代理。

这种情况下,只需要将https://github.com/aya-rs/aya-template clone 到本地,然后

1
cargo generate -p aya-template

即可。

输入项目名后,我们可以选择众多,bpf程序类型的模板 ,这里面有用于内核追踪的,流控的,xdp等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
🔧   Destination: /home/fenix/rust/asdf ...
🔧 project-name: asdf ...
🔧 Generating template ...
? 🤷 Which type of eBPF program? ›
cgroup_skb
cgroup_sockopt
cgroup_sysctl
classifier
fentry
fexit
kprobe
kretprobe
lsm
perf_event
raw_tracepoint
sk_msg
sock_ops
socket_filter
tp_btf
tracepoint
uprobe
uretprobe
❯ xdp

我们先选择xdp试试:

xdp

选择xdp后,我们可以看到生成的项目结构(我这里项目名为xdp-bpf-demo)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

├── Cargo.toml
├── README.md
├── xdp-bpf-demo
│   ├── Cargo.toml
│   └── src
│   └── main.rs
├── xdp-bpf-demo-common
│   ├── Cargo.toml
│   └── src
│   └── lib.rs
├── xdp-bpf-demo-ebpf
│   ├── Cargo.toml
│   ├── rust-toolchain.toml
│   └── src
│   └── main.rs
└── xtask
├── Cargo.toml
└── src
├── build_ebpf.rs
├── main.rs
└── run.rs

其中xdp-bpf-demo-common是公共模块,xtask 构建脚本,xdp-bpf-demo-ebpf 是内核端,xdp-bpf-demo 是用户端。

cargo generate为我们生成的README中包含了我们需要的命令。
我们先跑起来测试下:

构建内核端

1
cargo xtask build-ebpf

构建用户端

1
cargo build

启动,这里iface后面的参数是你的网卡名

1
RUST_LOG=info cargo xtask run -- --iface wlp2s0

不出意外的话就能看到:

1
2
3
4
5
[2022-12-21T18:03:09Z INFO  xdp_hello] Waiting for Ctrl-C...
[2022-12-21T18:03:11Z INFO xdp_hello] received a packet
[2022-12-21T18:03:11Z INFO xdp_hello] received a packet
[2022-12-21T18:03:11Z INFO xdp_hello] received a packet
[2022-12-21T18:03:11Z INFO xdp_hello] received a packet

现在我们回过头来看看项目里都有什么

内核端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

#![no_std]
#![no_main]

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

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

fn try_xdp_bpf_demo(ctx: XdpContext) -> Result<u32, u32> {
info!(&ctx, "received a packet");
Ok(xdp_action::XDP_PASS)
}

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

可以看到几个信息,首先内核端 是非std环境,所以不能调用alloc和collections 这些,只能使用core中的API了。非std也意味着,eBPF内核端不能调用系统调用。

内核的数据必须通过Map 传到 用户端。

解包

在内核端中加入一个新crate用于解包

1
network-types = "0.0.4"

我们的模板项目中给出了这样一个方法:

1
2
3
4
5
6
pub fn xdp_bpf_demo(ctx: XdpContext) -> u32 {
match try_xdp_bpf_demo(ctx) {
Ok(ret) => ret,
Err(_) => xdp_action::XDP_ABORTED,
}
}

XdpContext中有两个函数data() ,data_end() 我们需要将其转为指针,进一步解析。
我们引入这样一个函数,尝试将data()转为Rust裸指针。

1
2
3
4
5
6
7
8
9
10
11
12
#[inline(always)] // 
fn ptr_at<T>(ctx: &XdpContext, offset: usize) -> Result<*const T, ()> {
let start = ctx.data();
let end = ctx.data_end();
let len = mem::size_of::<T>();

if start + offset + len > end {
return Err(());
}

Ok((start + offset) as *const T)
}

尝试转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
fn try_xdp_bpf_demo(ctx: XdpContext) -> Result<u32, ()> {
// 转为以太头
let ethhdr: *const EthHdr = ptr_at(&ctx, 0)?; //
// 如果不是ipv4则放通,否则继续
match unsafe { (*ethhdr).ether_type } {
EtherType::Ipv4 => {}
_ => return Ok(xdp_action::XDP_PASS),
}

// 跳过以太头解析ipv4头
let ipv4hdr: *const Ipv4Hdr = ptr_at(&ctx, EthHdr::LEN)?;
// 取出ipv4地址
let source_addr = u32::from_be(unsafe { (*ipv4hdr).src_addr });
// 取出ipv4 端口
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(()),
};

// 打印 这里的打印实际上也是通过Map传输到用户端
info!(
&ctx,
"SRC IP: {:ipv4}, SRC PORT: {}", source_addr, source_port
);

Ok(xdp_action::XDP_PASS)
}

同样的方式编译执行,就可以看到抓包效果

1
2
3
4
5
6
7
[2023-02-28T13:57:32Z INFO  xdp_bpf_demo] Waiting for Ctrl-C...
[2023-02-28T13:57:32Z INFO xdp_bpf_demo] SRC IP: 192.168.31.202, SRC PORT: 54915
[2023-02-28T13:57:32Z INFO xdp_bpf_demo] SRC IP: 143.42.173.181, SRC PORT: 443
[2023-02-28T13:57:32Z INFO xdp_bpf_demo] SRC IP: 143.42.173.181, SRC PORT: 443
[2023-02-28T13:57:32Z INFO xdp_bpf_demo] SRC IP: 114.114.114.114, SRC PORT: 53
[2023-02-28T13:57:32Z INFO xdp_bpf_demo] SRC IP: 114.114.114.114, SRC PORT: 53
[2023-02-28T13:57:32Z INFO xdp_bpf_demo] SRC IP: 143.42.173.181, SRC PORT: 443

丢包

我们目前尝试了打印包,那么如何丢包呢?我们来通过*内核端和用户端的交互来实现一个防火墙,这个防火墙能够将黑名单内的ip的包丢弃,其余放通。

我们对我们的代码做些小改造,首先是内核端需要配置MAP

1
2
3
4
5
6
#[map(name = "BLOCKLIST")]
static mut BLOCKLIST: HashMap<u32, u32> = HashMap::<u32, u32>::with_max_entries(1024, 0);

fn is_blockip(addr: u32) -> bool {
unsafe { BLOCKLIST.get(&addr).is_some() }
}

解包部分,如果在黑名单中就Drop,如果不在黑名单中则pass。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn try_xdp_bpf_demo(ctx: XdpContext) -> Result<u32, ()> {
let ethhdr: *const EthHdr = ptr_at(&ctx, 0)?; //

match unsafe { (*ethhdr).ether_type } {
EtherType::Ipv4 => {}
_ => return Ok(xdp_action::XDP_PASS),
}

let ipv4hdr: *const Ipv4Hdr = ptr_at(&ctx, EthHdr::LEN)?;
let source_addr = u32::from_be(unsafe { (*ipv4hdr).src_addr });
let action = if is_blockip(source_addr) {
xdp_action::XDP_DROP
} else {
xdp_action::XDP_PASS
};
info!(&ctx, "SRC: {:ipv4}, ACTION: {}", source_addr, action);
Ok(xdp_action::XDP_PASS)
}

用户端部分也要改造,我们要将MAP配置到用户端中,才能实现在用户端中动态配置黑名单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
use std::net::Ipv4Addr;

use anyhow::Context;
use aya::maps::HashMap;
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 = "wlp2s0")]
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/xdp-bpf-demo"
))?;
#[cfg(not(debug_assertions))]
let mut bpf = Bpf::load(include_bytes_aligned!(
"../../target/bpfel-unknown-none/release/xdp-bpf-demo"
))?;
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_bpf_demo").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")?;

let mut block_list: HashMap<_, u32, u32> = HashMap::try_from(bpf.map_mut("BLOCKLIST")?)?;
let block_addr: u32 = Ipv4Addr::new(110, 242, 68, 66).try_into()?;
block_list.insert(block_addr, 0, 0)?;

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

Ok(())
}

这里固定配置了黑名单(baidu.com百度的ip 110.242.68.66),也可以设计为从stdin中读取更新,或者api。

同样 RUST_LOG=info cargo xtask run

然后就可以curl baidu.com测试下 ,当然需要注意的是,如果你开了代理需要加上-no-proxy参数,否则将会和本地ip,端口建立链接不会走黑名单。

1
2
3
4
5
6
7
8
9
10
11
12
[2023-02-28T16:36:12Z INFO  xdp_bpf_demo] SRC: 192.168.31.202, ACTION: 2
[2023-02-28T16:36:13Z INFO xdp_bpf_demo] SRC: 192.168.31.202, ACTION: 2
[2023-02-28T16:36:14Z INFO xdp_bpf_demo] SRC: 192.168.31.202, ACTION: 2
[2023-02-28T16:36:15Z INFO xdp_bpf_demo] SRC: 192.168.31.202, ACTION: 2
[2023-02-28T16:36:16Z INFO xdp_bpf_demo] SRC: 110.242.68.66, ACTION: 1
[2023-02-28T16:36:16Z INFO xdp_bpf_demo] SRC: 110.242.68.66, ACTION: 1
[2023-02-28T16:36:16Z INFO xdp_bpf_demo] SRC: 110.242.68.66, ACTION: 1
[2023-02-28T16:36:16Z INFO xdp_bpf_demo] SRC: 192.168.31.202, ACTION: 2
[2023-02-28T16:36:17Z INFO xdp_bpf_demo] SRC: 114.38.97.176, ACTION: 2
[2023-02-28T16:36:17Z INFO xdp_bpf_demo] SRC: 192.168.31.202, ACTION: 2
[2023-02-28T16:36:18Z INFO xdp_bpf_demo] SRC: 143.42.173.181, ACTION: 2
[2023-02-28T16:36:18Z INFO xdp_bpf_demo] SRC: 192.168.31.202, ACTION: 2