Skip to content

解析数据包

在上一章中,我们的XDP应用程序运行直到按下Ctrl-C,并允许所有流量。每次接收到数据包时,eBPF程序会记录字符串"received a packet"。在本章中,我们将展示如何解析数据包。

虽然我们可以深入解析到L7,但我们将把示例限制在L3,并且为了简化,只处理IPv4。

源代码

本章示例的完整代码可在此处找到。

使用网络类型

我们将记录传入数据包的源IP地址。因此,我们需要:

  • 读取以太网头以确定是否处理IPv4数据包,否则终止解析。
  • 从IPv4头读取源IP地址。

我们可以查阅这些协议的规范并手动解析,但我们将使用network-types crate,它提供了许多常见互联网协议的便捷类型定义。

让我们通过在xdp-log-ebpf/Cargo.toml中添加对network-types的依赖,将其添加到我们的eBPF crate中:

[package]
name = "xdp-log-ebpf"
version = "0.1.0"
edition = "2021"

[dependencies]
aya-ebpf = { git = "https://github.com/aya-rs/aya" }
aya-log-ebpf = { git = "https://github.com/aya-rs/aya" }
xdp-log-common = { path = "../xdp-log-common" }
network-types = "0.0.4"

[[bin]]
name = "xdp-log"
path = "src/main.rs"

[profile.dev]
opt-level = 3
debug = false
debug-assertions = false
overflow-checks = false
lto = true
panic = "abort"
incremental = false
codegen-units = 1
rpath = false

[profile.release]
lto = true
panic = "abort"
codegen-units = 1

[workspace]
members = []

从上下文获取数据包数据

XdpContext包含我们将使用的两个字段:datadata_end,它们分别是指向数据包开始和结束的指针。

为了访问数据包中的数据并确保以使eBPF验证器满意的方式进行,我们将引入一个名为ptr_at的辅助函数。该函数确保在访问任何数据包数据之前,我们插入验证器所需的边界检查。

最后,为了访问以太网和IPv4头的各个字段,我们将使用memoffset crate,让我们在xdp-log-ebpf/Cargo.toml中为其添加依赖。

使用offset_of!读取字段

由于堆栈空间有限,使用offset_of!宏读取结构体中的单个字段比读取整个结构体并通过名称访问字段更节省内存。

生成的代码如下所示:

xdp-log-ebpf/src/main.rs
#![no_std]
#![no_main]

use aya_ebpf::{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,
};

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

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

#[inline(always)] // (1)
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)
}

fn try_xdp_firewall(ctx: XdpContext) -> Result<u32, ()> {
    let ethhdr: *const EthHdr = ptr_at(&ctx, 0)?; // (2)
    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 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(()),
    };

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

    Ok(xdp_action::XDP_PASS)
}
  1. 在这里我们定义ptr_at以确保数据包访问总是进行边界检查。
  2. 使用ptr_at读取我们的以太网头。
  3. 在这里我们记录IP和端口。

不要忘记重新构建您的eBPF程序!

用户空间组件

我们的用户空间代码与上一章没有太大区别,但为了参考,以下是代码:

xdp-log/src/main.rs
use anyhow::Context;
use aya::{
    include_bytes_aligned,
    programs::{Xdp, XdpFlags},
    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();

    // This will include your eBPF object file as raw bytes at compile-time and load it at
    // runtime. This approach is recommended for most real-world use cases. If you would
    // like to specify the eBPF program at runtime rather than at compile-time, you can
    // reach for `Bpf::load_file` instead.
    #[cfg(debug_assertions)]
    let mut bpf = Bpf::load(include_bytes_aligned!(
        "../../target/bpfel-unknown-none/debug/xdp-log"
    ))?;
    #[cfg(not(debug_assertions))]
    let mut bpf = Bpf::load(include_bytes_aligned!(
        "../../target/bpfel-unknown-none/release/xdp-log"
    ))?;
    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_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")?;

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

    Ok(())
}

运行程序

与之前一样,可以通过提供接口名称作为参数来覆盖接口,例如,RUST_LOG=info cargo xtask run -- --iface wlp2s0

$ RUST_LOG=info cargo xtask run
[2022-12-22T11:32:21Z INFO  xdp_log] SRC IP: 172.52.22.104, SRC PORT: 443
[2022-12-22T11:32:21Z INFO  xdp_log] SRC IP: 172.52.22.104, SRC PORT: 443
[2022-12-22T11:32:21Z INFO  xdp_log] SRC IP: 172.52.22.104, SRC PORT: 443
[2022-12-22T11:32:21Z INFO  xdp_log] SRC IP: 172.52.22.104, SRC PORT: 443
[2022-12-22T11:32:21Z INFO  xdp_log] SRC IP: 234.130.159.162, SRC PORT: 443

每次接收到数据包时,程序会记录其源IP地址和端口。