
Rust + eBPF 已经彻底火出圈,成为云原生、可观测性、网络安全、边缘计算领域最香的组合技。eBPF 技术允许你在 Linux 内核中运行自定义程序,而 Rust 的内存安全特性让开发过程更可靠,避免了传统 C 语言常见的内核崩溃风险。
”今天直接用目前最现代化、最好上手的 Aya 框架,带你从零写两个真实可运行的 socket filter 示例,全中文详细注释!此外,还实现了一个进阶案例:使用 eBPF map 实现 IP 黑名单过滤,超级实用!
读完这篇,你将掌握:
先来看看 eBPF socket filter 的架构图,帮助你直观理解数据包从内核到用户态的过滤过程:

走起~直接上干货!
2026年,Rust 已经升级到 1.85+,原生支持更先进的 BPF target(如 bpfel-unknown-none 的增强版),Aya 框架也迭代到 0.12.x,新增了 async 支持和更复杂的 map 操作。 Aya 是纯 Rust 方案,无需 libbpf、无需 clang、cargo 一键构建,开发体验吊打传统方式。
一句话:用 Rust 写 eBPF,你既能享受 C 的极致性能,又能拥有现代语言的安全与优雅。
# 切换到 nightly(Aya 目前仍依赖 nightly,但2026年可能稳定版支持)
rustup toolchain install nightly
rustup default nightly
rustup target add bpfel-unknown-none
# 安装 Aya 项目生成器(强烈推荐)
cargo install cargo-generate --locked
# 生成项目模板,选择 socket filter 类型
cargo generate https://github.com/aya-rs/aya-template
# 项目名随便起,比如:ebpf-rust-socket
# Program type 选:socket生成后目录结构:
ebpf-rust-socket/
├── Cargo.toml
├── ebpf/ # ← 内核 eBPF 代码在这里
│ └── src/main.rs
└── src/ # ← 用户态加载代码
└── main.rs提示:确保你的内核版本 >= 5.10,支持最新 Aya 特性。如果遇到 verifier 错误,检查 rustc 版本。
目标:附加到一个 socket 上,只允许 IPv4 包通过,其他协议(IPv6、ARP 等)全部丢弃。
ebpf/src/main.rs(内核 eBPF 程序)
#![no_std] // 内核环境没有 std 库,必须禁用
#![no_main] // 禁用 Rust 默认的 main 入口,由 Aya 提供入口
use aya_ebpf::{ // Aya 提供的 eBPF 核心工具集
macros::socket_filter, // 声明 socket filter 程序的宏
programs::SocketFilterContext, // socket filter 的上下文,包含 skb(数据包缓冲区)
bindings::ETH_P_IP, // 内核常量:以太网类型 IPv4 = 0x0800
};
#[socket_filter] // Aya 宏:标记这是一个 socket filter 类型的 eBPF 程序
pub fn ipv4_only_filter(ctx: SocketFilterContext) -> i32 {
// 获取以太网头部,返回 Option<EthHdr>,非常安全
let eth = ctx.eth();
// 如果数据包太短或没有以太网头,直接丢弃(返回 0 = 丢弃整个包)
if eth.is_none() {
return 0;
}
// 前面已判断 is_some(),这里 unwrap 是安全的
let eth = eth.unwrap();
// 读取以太网协议类型字段(偏移 12-13 字节,大端序)
// 如果不是 IPv4 (0x0800),直接丢弃
if eth.proto() != ETH_P_IP as u16 {
return 0;
}
// 是 IPv4 包,放行全部字节
// 返回值 = 要保留的字节数,返回整个 skb 长度 = 完整放行
ctx.skb().len() as i32
}
// eBPF 程序必须定义 panic handler,否则 verifier 会拒绝加载
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
// 在 eBPF 中不能打印 panic 信息,只能让程序“死掉”
unsafe { core::hint::unreachable_unchecked() }
}src/main.rs(用户态加载 + 附加程序)
use aya::{ // Aya 用户态核心库
include_bytes_aligned, // 编译时安全嵌入 .o 文件的宏(保证对齐)
programs::{SocketFilter, SocketFilterLink}, // socket filter 类型和链接
Ebpf, // eBPF 对象管理器
};
use std::os::unix::io::AsRawFd; // 提供 as_raw_fd() trait
use tokio::net::TcpListener; // 用 tokio 创建测试用的 TCP socket
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
// 加载编译好的 eBPF 对象(路径由 cargo-aya 自动管理)
let mut ebpf = Ebpf::load(
include_bytes_aligned!("../../target/bpfel-unknown-none/debug/ebpf")
)?;
// 根据函数名找到程序(程序名 = 函数名)
let prog: &mut SocketFilter = ebpf
.program_mut("ipv4_only_filter") // 注意与内核函数名一致
.unwrap()
.try_into()?;
// 加载程序到内核(验证 + JIT 编译)
prog.load()?;
// 创建一个本地 TCP listener,仅用于获取 fd(端口 0 = 系统自动分配)
let listener = TcpListener::bind("127.0.0.1:0").await?;
let sock_fd = listener.as_raw_fd();
// 把 eBPF filter 附加到这个 socket
// 从此以后,通过这个 socket 收到的所有包都会先经过我们的 filter
let link_id = prog.attach(sock_fd)?;
println!("【成功】IPv4-only socket filter 已附加到 fd {}", sock_fd);
println!("测试方法:用 nc -u 127.0.0.1 <port> 发 UDP / IPv6 包,应该都被丢弃~");
// 等待 Ctrl+C 优雅退出
tokio::signal::ctrl_c().await?;
// 清理:detach 程序,避免残留
let _ = prog.detach(link_id);
Ok(())
}编译运行:
cargo build --release
sudo target/release/ebpf-rust-socket目标:只允许源端口或目的端口为 80 的 TCP 包通过,其他丢弃。
ebpf/src/main.rs(内核部分)
#![no_std]
#![no_main]
use aya_ebpf::{
macros::socket_filter,
programs::SocketFilterContext,
bindings::{ETH_P_IP, IPPROTO_TCP},
};
use aya_ebpf::bindings::{iphdr, tcphdr}; // IP 和 TCP 头部结构体定义
use core::mem;
#[socket_filter]
pub fn http_port_filter(ctx: SocketFilterContext) -> i32 {
// 第一层:解析以太网头
let eth = match ctx.eth() {
Some(eth) => eth,
None => return 0, // 包太短,丢弃
};
// 不是 IPv4,直接丢弃
if eth.proto() != ETH_P_IP as u16 {
return 0;
}
// 第二层:解析 IP 头(Aya 提供便捷方法)
let ip = match ctx.ip() {
Some(ip) => ip,
None => return 0,
};
// 不是 TCP 协议,丢弃
if ip.proto() != IPPROTO_TCP as u8 {
return 0;
}
// 第三层:计算 TCP 头偏移(这里简化假设无 IP 选项,固定 20 字节)
// 生产环境应读取 ip.ihl() * 4
let tcp_offset = mem::size_of::<iphdr>() as u64;
// 从 skb 指定偏移加载 TCP 头部(load 方法带边界检查)
let tcp = match ctx.load::<tcphdr>(tcp_offset) {
Ok(tcp) => tcp,
Err(_) => return 0, // 包太短或偏移错误,丢弃
};
// TCP 端口是大端序,转为主机序
let src_port = u16::from_be(tcp.source);
let dst_port = u16::from_be(tcp.dest);
// 判断是否为 HTTP 流量(源或目的端口为 80)
if src_port == 80 || dst_port == 80 {
// 放行整个包
ctx.skb().len() as i32
} else {
// 其他端口,丢弃
0
}
}
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
unsafe { core::hint::unreachable_unchecked() }
}用户态部分只需把程序名改成 "http_port_filter",其余代码与案例1完全相同:
let prog: &mut SocketFilter = ebpf.program_mut("http_port_filter").unwrap().try_into()?;目标:基于 HashMap 黑名单,动态阻塞特定 IPv4 地址的流量。用户态可以实时更新 map,实现可配置防火墙。 这在云环境(如过滤云元数据服务)超级实用!
ebpf/src/main.rs(内核部分)
#![no_std]
#![no_main]
use aya_ebpf::{
macros::{socket_filter, map},
programs::SocketFilterContext,
maps::HashMap,
bindings::{ETH_P_IP, __be32},
};
#[map] // Aya 宏:定义一个 HashMap,键为 u32 (IPv4),值为 u8 (dummy)
pub static mut BLOCKLIST: HashMap<__be32, u8> = HashMap::with_max_entries(1024, 0);
#[socket_filter]
pub fn ip_block_filter(ctx: SocketFilterContext) -> i32 {
// 解析以太网头(简化,只检查 IPv4)
let eth = match ctx.eth() {
Some(eth) => eth,
None => return 0,
};
if eth.proto() != ETH_P_IP as u16 {
return 0;
}
// 解析 IP 头
let ip = match ctx.ip() {
Some(ip) => ip,
None => return 0,
};
// 检查目的 IP 是否在黑名单中(大端序)
let dst_ip = ip.dst_addr();
unsafe {
if BLOCKLIST.get(&dst_ip).is_some() {
return 0; // 在黑名单,丢弃
}
}
// 不阻塞,放行
ctx.skb().len() as i32
}
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
unsafe { core::hint::unreachable_unchecked() }
}src/main.rs(用户态:加载 + 更新 map)
use aya::{Ebpf, maps::HashMap as AyaHashMap, programs::{SocketFilter, SocketFilterLink}};
use aya::include_bytes_aligned;
use std::net::Ipv4Addr;
use std::os::unix::io::AsRawFd;
use tokio::net::TcpListener;
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
let mut ebpf = Ebpf::load(include_bytes_aligned!("../../target/bpfel-unknown-none/debug/ebpf"))?;
let prog: &mut SocketFilter = ebpf.program_mut("ip_block_filter").unwrap().try_into()?;
prog.load()?;
let listener = TcpListener::bind("127.0.0.1:0").await?;
let sock_fd = listener.as_raw_fd();
let link_id = prog.attach(sock_fd)?;
// 获取 map 并添加黑名单 IP(例如 192.168.1.1)
let mut blocklist: AyaHashMap<_, u32, u8> = AyaHashMap::try_from(ebpf.map_mut("BLOCKLIST").unwrap())?;
let blocked_ip: u32 = Ipv4Addr::new(192, 168, 1, 1).to_bits(); // 主机序转大端
blocklist.insert(&blocked_ip, &1, 0)?;
println!("IP 黑名单 filter 已附加,并阻塞了 192.168.1.1");
tokio::signal::ctrl_c().await?;
let _ = prog.detach(link_id);
Ok(())
}这个案例展示了 eBPF map 的强大:用户态动态配置内核行为,无需重载程序!测试时,用 nc 发送到黑名单 IP,看是否被丢。
let ihl = ip.ihl() as u64 * 4; 计算真实 tcp_offset。aya_log_ebpf::info!(&ctx, "msg"); 打印到 /sys/kernel/debug/tracing/trace_pipe。三个完整、可直接运行的 socket filter 示例已经给你了,注释拉满,基本覆盖了新手最容易踩的坑。2026年,eBPF 已经是内核开发的标配,配合上 AI,Rust 让它更易上手。
推荐继续进阶方向:
资源直达:
喜欢这篇?点个在看 + 点赞 + 转发三连支持一下!关注公众号“Rust火箭工坊”,下期我们继续用 Rust + eBPF 搞更硬核的东西~