Skip to content

分类器

源代码

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

eBPF中的分类器是什么?

分类器是一种eBPF程序,它附加到Linux内核网络中的排队学科(通常称为qdisc),因此可以对与qdisc关联的网络接口上接收到的数据包做出决策。

对于每个网络接口,入站和出站流量都有单独的qdisc。当将分类器程序附加到接口时,

分类器和XDP有什么区别?

  • 分类器比XDP更早出现,自内核4.1版本起可用,而XDP自4.8版本起可用。
  • 分类器可以检查入站和出站流量。XDP仅限于入站。
  • XDP提供更好的性能,因为它执行得更早——在数据包到达任何内核网络栈层并解析为sk_buff结构之前,它接收到的是来自NIC驱动的原始数据包。

示例项目

为了与XDP示例有所不同,我们尝试编写一个允许丢弃出站流量的程序。

设计

我们将:

  • 创建一个HashMap,用作阻止列表。
  • 从数据包中检查目的IP地址,并根据HashMap做出策略决策(通过或丢弃)。
  • 从用户空间向阻止列表中添加条目。

eBPF代码

程序代码将从定义BLOCKLIST映射开始。为了强制执行策略,程序将在该映射中查找目的IP地址。如果该地址的映射条目存在,我们将丢弃数据包。否则,我们将使用TC_ACT_PIPE动作传递它——这意味着在我们这一侧允许它,但也让其他分类器程序和qdisc过滤器检查数据包。

TC_ACT_OK

还有一种可能性是允许数据包绕过其他程序和过滤器——TC_ACT_OK。我们建议只有在绝对确定您希望您的程序优先于其他程序或过滤器时才使用该选项。

以下是eBPF代码的样子:

tc-egress-ebpf/src/main.rs
#![no_std]
#![no_main]

use aya_ebpf::{
    bindings::{TC_ACT_PIPE, TC_ACT_SHOT},
    macros::{classifier, map},
    maps::HashMap,
    programs::TcContext,
};
use aya_log_ebpf::info;
use network_types::{
    eth::{EthHdr, EtherType},
    ip::Ipv4Hdr,
};

#[map]
static BLOCKLIST: HashMap<u32, u32> = HashMap::with_max_entries(1024, 0);

#[classifier]
pub fn tc_egress(ctx: TcContext) -> i32 {
    match try_tc_egress(ctx) {
        Ok(ret) => ret,
        Err(_) => TC_ACT_SHOT,
    }
}

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

fn try_tc_egress(ctx: TcContext) -> Result<i32, ()> {
    let ethhdr: EthHdr = ctx.load(0).map_err(|_| ())?;
    match ethhdr.ether_type {
        EtherType::Ipv4 => {}
        _ => return Ok(TC_ACT_PIPE),
    }

    let ipv4hdr: Ipv4Hdr = ctx.load(EthHdr::LEN).map_err(|_| ())?;
    let destination = u32::from_be(ipv4hdr.dst_addr);

    let action = if block_ip(destination) {
        TC_ACT_SHOT
    } else {
        TC_ACT_PIPE
    };

    info!(&ctx, "DEST {:i}, ACTION {}", destination, action);

    Ok(action)
}

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    unsafe { core::hint::unreachable_unchecked() }
}
  1. 创建我们的映射。
  2. 检查是否应该允许或拒绝数据包。
  3. 返回正确的操作。

用户空间代码

用户空间代码的目的是加载eBPF程序,将其附加到给定的网络接口,然后用要阻止的地址填充映射。

在此示例中,我们将阻止所有出站到1.1.1.1的流量。

以下是代码的样子:

tc-egress/src/main.rs
use std::net::Ipv4Addr;

use aya::{
    include_bytes_aligned,
    maps::HashMap,
    programs::{tc, SchedClassifier, TcAttachType},
    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/tc-egress"
    ))?;
    #[cfg(not(debug_assertions))]
    let mut bpf = Bpf::load(include_bytes_aligned!(
        "../../target/bpfel-unknown-none/release/tc-egress"
    ))?;
    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);
    }
    // error adding clsact to the interface if it is already added is harmless
    // the full cleanup can be done with 'sudo tc qdisc del dev eth0 clsact'.
    let _ = tc::qdisc_add_clsact(&opt.iface);
    let program: &mut SchedClassifier =
        bpf.program_mut("tc_egress").unwrap().try_into()?;
    program.load()?;
    program.attach(&opt.iface, TcAttachType::Egress)?;

    // (1)
    let mut blocklist: HashMap<_, u32, u32> =
        HashMap::try_from(bpf.map_mut("BLOCKLIST").unwrap())?;

    // (2)
    let block_addr: u32 = Ipv4Addr::new(1, 1, 1, 1).try_into()?;

    // (3)
    blocklist.insert(block_addr, 0, 0)?;

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

    Ok(())
}
  1. 获取映射的引用。
  2. 创建一个IPv4Addr。
  3. 用我们希望阻止出站流量的远程IP地址填充映射。

第三步是通过获取BLOCKLIST映射的引用并调用blocklist.insert完成的。在Rust中使用IPv4Addr类型将允许我们读取IP地址的易读表示并将其转换为u32,这是在eBPF映射中使用的适当类型。

运行程序

$ RUST_LOG=info cargo xtask run
LOG: SRC 1.1.1.1, ACTION 2
LOG: SRC 35.186.224.47, ACTION 3
LOG: SRC 35.186.224.47, ACTION 3
LOG: SRC 1.1.1.1, ACTION 2
LOG: SRC 168.100.68.32, ACTION 3
LOG: SRC 168.100.68.239, ACTION 3
LOG: SRC 168.100.68.32, ACTION 3
LOG: SRC 168.100.68.239, ACTION 3
LOG: SRC 1.1.1.1, ACTION 2
LOG: SRC 13.248.212.111, ACTION 3