XDP
源代码
本章示例的完整代码可在此处找到。
什么是eBPF中的XDP?
XDP(eXpress Data Path)是一种eBPF程序,附加到网络接口。 它使得能够在网络数据包从网络驱动接收时立即进行过滤、操作及重定向, 甚至在它们进入Linux内核网络栈之前,从而实现低延迟和高吞吐量。
XDP的思想是在内核的RX路径中添加一个早期钩子,
并让用户提供的eBPF程序决定数据包的命运。
该钩子放置在NIC驱动中,紧接中断处理之后,
并且在任何网络栈自身所需的内存分配之前。
XDP程序允许编辑数据包数据, 在XDP程序返回后,动作代码决定如何处理数据包:
XDP_PASS: 让数据包继续通过网络栈XDP_DROP: 静默丢弃数据包XDP_ABORTED: 丢弃数据包并记录异常XDP_TX: 将数据包返回到其到达的同一NICXDP_REDIRECT: 通过AF_XDP地址族将数据包重定向到另一个NIC或用户空间套接字
AF_XDP
随着XDP的出现,Linux内核在4.18版本中引入了一个新的地址族。
AF_XDP,以前称为AF_PACKETv4(从未包含在主线内核中),
是一种为高性能数据包处理而优化的原始套接字,
允许在内核和应用程序之间进行零拷贝。
由于套接字可以用于接收和发送,
它支持在用户空间中纯粹运行的高性能网络应用程序。
如果您想要关于AF_XDP的更详细的解释,
可以在内核文档中找到。
XDP操作模式
您可以使用以下模式将XDP程序连接到接口:
通用XDP
- XDP程序作为普通网络路径的一部分加载到内核中
- 不需要网络卡驱动的支持即可运行
- 不提供完整的性能优势
- 测试XDP程序的简单方法
原生XDP
- XDP程序由网络卡驱动作为其初始接收路径的一部分加载
- 需要网络卡驱动的支持才能运行
- 默认操作模式
卸载XDP
- XDP程序直接加载到NIC上,并在不使用CPU的情况下执行
- 需要NIC的支持
驱动支持原生XDP
支持原生XDP的驱动的列表可以在下表中找到:
| 厂商 | 驱动 | XDP支持版本 |
|---|---|---|
| Amazon | ena | >=5.6 |
| Broadcom | bnxt_en | >=4.11 |
| Cavium | thunderx | >=4.12 |
| Freescale | dpaa2 | >=5.0 |
| Intel | ixgbe | >=4.12 |
| Intel | ixgbevf | >=4.17 |
| Intel | i40e | >=4.13 |
| Intel | ice | >=5.5 |
| Marvell | mvneta | >=5.5 |
| Mellanox | mlx4 | >=4.8 |
| Mellanox | mlx5 | >=4.9 |
| Microsoft | hv_netvsc | >=5.6 |
| Netronome | nfp | >=4.10 |
| Others | virtio_net | >=4.10 |
| Others | tun/tap | >=4.14 |
| Others | bond | >=5.15 |
| Qlogic | qede | >=4.10 |
| Socionext | netsec | >=5.3 |
| Solarflare | sfc | >=5.5 |
| Texas Instruments | cpsw | >=5.3 |
您可以使用以下命令检查接口的网络驱动程序名称:
ethtool -i <interface>。
驱动支持卸载XDP
目前,仅Netronome NFP驱动支持卸载XDP。
示例项目
现在您对XDP有了一些了解,让我们继续一个实际的例子。 我们将编写一个简单的XDP程序,用于丢弃来自某些IP的数据包。
设置开发环境
确保您已经具备前提条件。
由于我们正在编写一个XDP程序,我们将使用XDP模板(通过cargo generate创建):
创建eBPF组件
首先,我们必须为我们的程序创建eBPF组件, 在这个组件中,我们将决定如何处理传入的数据包。
由于我们想要丢弃来自某些IP的传入数据包,
我们将在IP在我们的黑名单中时使用XDP_DROP动作代码,
对于其他所有情况将使用XDP_PASS动作代码。
#![no_std]
#![no_main]
use aya_ebpf::{
bindings::xdp_action,
macros::{map, xdp},
maps::HashMap,
programs::XdpContext,
};
use aya_log_ebpf::info;
use core::mem;
use network_types::{
eth::{EthHdr, EtherType},
ip::Ipv4Hdr,
};
我们导入了必要的依赖项:
aya_ebpf: 对于XDP动作(bindings::xdp_action)、XDP上下文结构XdpContext(programs:XdpContext), 映射定义(对于我们的HashMap)和XDP程序宏(macros::{map, xdp})aya_log_ebpf: 用于在eBPF程序中进行日志记录core::mem: 用于内存操作network_types: 用于以太网和IP头的定义
重要
确保在您的Cargo.toml中添加network_types依赖项。
以下是代码的样子:
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
unsafe { core::hint::unreachable_unchecked() }
}
提供了一个eBPF兼容的panic处理程序,因为 eBPF程序不能使用默认的panic行为。
在这里,我们定义了一个带有HashMap的黑名单,
它存储整数(u32),最多可存储1024个条目。
#[xdp]
pub fn xdp_firewall(ctx: XdpContext) -> u32 {
match try_xdp_firewall(ctx) {
Ok(ret) => ret,
Err(_) => xdp_action::XDP_ABORTED,
}
}
xdp_firewall函数(在用户空间中获取)接受XdpContext并返回一个u32。
它将主要的数据包处理逻辑委托给try_xdp_firewall函数。
如果发生错误,该函数返回xdp_action::XDP_ABORTED(等同于u32 0)。
#[inline(always)]
unsafe 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(());
}
let ptr = (start + offset) as *const T;
Ok(&*ptr)
}
我们的ptr_at函数旨在提供安全访问XdpContext中指定偏移量的泛型类型T。
它通过将所需的内存范围(start + offset + len)与数据的结束(end)进行比较来执行边界检查。
如果访问在边界内,它返回指向指定类型的指针;否则,
返回错误。我们将使用此函数从XdpContext中检索数据。
fn block_ip(address: u32) -> bool {
unsafe { BLOCKLIST.get(&address).is_some() }
}
fn try_xdp_firewall(ctx: XdpContext) -> Result<u32, ()> {
let ethhdr: *const EthHdr = unsafe { ptr_at(&ctx, 0)? };
match unsafe { (*ethhdr).ether_type } {
EtherType::Ipv4 => {}
_ => return Ok(xdp_action::XDP_PASS),
}
let ipv4hdr: *const Ipv4Hdr = unsafe { ptr_at(&ctx, EthHdr::LEN)? };
let source = u32::from_be(unsafe { (*ipv4hdr).src_addr });
let action = if block_ip(source) {
xdp_action::XDP_DROP
} else {
xdp_action::XDP_PASS
};
info!(&ctx, "SRC: {:i}, ACTION: {}", source, action);
Ok(action)
}
block_ip函数检查给定的IP地址是否存在于黑名单中。
如前所述,try_xdp_firewall包含我们的防火墙的主要逻辑。
我们首先使用ptr_at函数从XdpContext中检索以太网头,
该头位于XdpContext的开头,因此我们使用0作为偏移量。
如果数据包不是IPv4(ether_type检查),该函数返回xdp_action::XDP_PASS并
允许数据包通过网络栈。
ipv4hdr用于检索IPv4头,source用于存储IPv4头中的源IP地址。
然后,我们使用之前创建的block_ip函数将IP地址与黑名单中的IP进行比较。
如果block_ip匹配,意味着IP在黑名单中,我们使用XDP_DROP动作代码以便它不会
通过网络栈,否则我们使用XDP_PASS动作代码让它通过。
最后,我们记录活动,SRC是源IP地址,ACTION
是对其使用的动作代码。然后返回Ok(action)作为结果。
完整代码:
#![no_std]
#![no_main]
#![allow(nonstandard_style, dead_code)]
use aya_ebpf::{
bindings::xdp_action,
macros::{map, xdp},
maps::HashMap,
programs::XdpContext,
};
use aya_log_ebpf::info;
use core::mem;
use network_types::{
eth::{EthHdr, EtherType},
ip::Ipv4Hdr,
};
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
unsafe { core::hint::unreachable_unchecked() }
}
#[map]
static IP_BLOCKLIST: HashMap<u32, u32> = HashMap::<u32, u32>::with_max_entries(1024, 0);
#[xdp]
pub fn xdp_firewall(ctx: XdpContext) -> u32 {
match try_xdp_firewall(ctx) {
Ok(ret) => ret,
Err(_) => xdp_action::XDP_ABORTED,
}
}
#[inline(always)]
unsafe 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(());
}
let ptr = (start + offset) as *const T;
Ok(&*ptr)
}
fn block_ip(address: u32) -> bool {
unsafe { IP_BLOCKLIST.get(&address).is_some() }
}
fn try_xdp_firewall(ctx: XdpContext) -> Result<u32, ()> {
let ethhdr: *const EthHdr = unsafe { ptr_at(&ctx, 0)? };
match unsafe { (*ethhdr).ether_type } {
EtherType::Ipv4 => {}
_ => return Ok(xdp_action::XDP_PASS),
}
let ipv4hdr: *const Ipv4Hdr = unsafe { ptr_at(&ctx, EthHdr::LEN)? };
let source = u32::from_be(unsafe { (*ipv4hdr).src_addr });
let action = if block_ip(source) {
xdp_action::XDP_DROP
} else {
xdp_action::XDP_PASS
};
info!(&ctx, "SRC: {:i}, ACTION: {}", source, action);
Ok(action)
}
从用户空间填充我们的映射
为了添加要阻止的地址,我们首先需要获取到BLOCKLIST映射的引用。
一旦我们拥有它,只需调用ip_blocklist.insert()即可
将IP插入到黑名单中。
我们将使用IPv4Addr类型来表示我们的IP地址,因为
它是易读的,可以轻松地转换为u32。
在这个例子中,我们将阻止所有来自1.1.1.1的流量。
字节序
IP地址始终在数据包中以网络字节顺序(大端)编码。在我们的eBPF程序中,在检查黑名单之前,我们使用u32::from_be将它们转换为主机字节序。因此,从用户空间以主机字节序格式编写我们的IP地址是正确的。
另一种方法也可以:我们可以在从用户空间插入时将IP转换为网络字节序,然后在从eBPF程序中索引时就不需要转换了。
让我们开始编写用户空间代码:
导入依赖项
use anyhow::Context;
use aya::{
include_bytes_aligned,
maps::HashMap,
programs::{Xdp, XdpFlags},
Ebpf,
};
use aya_log::EbpfLogger;
use clap::Parser;
use log::{info, warn};
use std::net::Ipv4Addr;
use tokio::signal;
anyhow::Context: 为错误处理提供附加的上下文aya: 提供用于加载eBPF程序的Bpf结构和相关函数, 以及XDP程序及其标志(aya::programs::{Xdp, XdpFlags})aya_log::EbpfLogger: 用于在eBPF程序中进行日志记录clap::Parser: 提供参数解析log::{info, warn}: 我们用于信息和警告消息的日志库std::net::Ipv4Addr: 用于处理IPv4地址的结构tokio::signal: 用于异步处理信号,更多信息请参见此链接
Note
aya::Bpf自版本0.13.0起被弃用,aya_log:BpfLogger自版本0.2.1起被弃用。
如果您使用更高版本,请使用aya::Ebpf和
aya_log:EbpfLogger代替。
定义命令行参数
使用clap的派生功能定义了一个用于命令行解析的简单结构,
其中可选参数iface用于提供我们的网络接口名称。
主函数
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
let opt = Opt::parse();
env_logger::init();
#[cfg(debug_assertions)]
let mut bpf = Ebpf::load(include_bytes_aligned!(
"../../target/bpfel-unknown-none/debug/simple-xdp-program"
))?;
#[cfg(not(debug_assertions))]
let mut bpf = Ebpf::load(include_bytes_aligned!(
"../../target/bpfel-unknown-none/release/xdp-simple-xdp-program"
))?;
if let Err(e) = EbpfLogger::init(&mut bpf) {
warn!("failed to initialize eBPF logger: {}", e);
}
let program: &mut Xdp =
bpf.program_mut("xdp_firewall").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 blocklist: HashMap<_, u32, u32> =
HashMap::try_from(bpf.map_mut("BLOCKLIST").unwrap())?;
let block_addr: u32 = Ipv4Addr::new(1, 1, 1, 1).try_into()?;
blocklist.insert(block_addr, 0, 0)?;
info!("Waiting for Ctrl-C...");
signal::ctrl_c().await?;
info!("Exiting...");
Ok(())
}
解析命令行参数
在main函数中,我们首先使用Opt::parse()和之前定义的结构解析命令行参数。
初始化环境日志
使用env_logger::init()初始化日志,
稍后我们将在代码中使用环境日志。
加载eBPF程序
使用Ebpf::load()加载eBPF程序,根据构建配置选择debug或
release版本(debug_assertions)。
加载和附加我们的XDP
从我们之前定义的eBPF程序中检索名为xdp_firewall的XDP程序
使用bpf.program_mut()。
然后加载XDP程序并将其附加到我们的网络接口。
设置IP黑名单
从eBPF程序加载IP黑名单(BLOCKLIST映射)并转换为HashMap。
将IP 1.1.1.1添加到黑名单中。
等待退出信号
程序使用signal::ctrl_c().await异步等待CTRL+C信号,
一旦收到信号,它会记录退出消息并返回Ok(())。
完整的用户空间代码
use anyhow::Context;
use aya::{
include_bytes_aligned,
maps::HashMap,
programs::{Xdp, XdpFlags},
Ebpf,
};
use aya_log::EbpfLogger;
use clap::Parser;
use log::{info, warn};
use std::net::Ipv4Addr;
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 = Ebpf::load(include_bytes_aligned!(
"../../target/bpfel-unknown-none/debug/simple-xdp-program"
))?;
#[cfg(not(debug_assertions))]
let mut bpf = Ebpf::load(include_bytes_aligned!(
"../../target/bpfel-unknown-none/release/xdp-simple-xdp-program"
))?;
if let Err(e) = EbpfLogger::init(&mut bpf) {
warn!("failed to initialize eBPF logger: {}", e);
}
let program: &mut Xdp =
bpf.program_mut("xdp_firewall").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 blocklist: HashMap<_, u32, u32> =
HashMap::try_from(bpf.map_mut("BLOCKLIST").unwrap())?;
let block_addr: u32 = Ipv4Addr::new(1, 1, 1, 1).try_into()?;
blocklist.insert(block_addr, 0, 0)?;
info!("Waiting for Ctrl-C...");
signal::ctrl_c().await?;
info!("Exiting...");
Ok(())
}
运行我们的程序!
现在我们已经拥有了eBPF程序的所有组件,可以使用以下命令运行它:RUST_LOG=info cargo xtask run
或RUST_LOG=info cargo xtask run -- --iface <interface>如果您想提供另一个网络接口名称,
请注意您也可以不带其余部分使用cargo xtask run,但不会有任何日志记录。