Full code for the example in this chapter is available here
What is Classifier in eBPF?
Classifier is a type of eBPF program which is attached to queuing disciplines
in Linux kernel networking (often referred to as qdisc) and therefore being
able to make decisions about packets that have been received on the network
interface associated with the qdisc.
For each network interface, there are separate qdiscs for ingress and egress
traffic. When attaching Classifier program to an interface,
What's the difference between Classifiers and XDP?
Classifier is older than XDP, it's available since kernel 4.1, while XDP -
since 4.8.
Classifier can inspect both ingress and egress traffic. XDP is limited to
ingress.
XDP provides better performance, because it's executed earlier - it receives
a raw packet from the NIC driver, before it goes to any layers of kernel
networking stack and gets parsed to the sk_buff structure.
Example project
To make a difference from the XDP example, let's try to write a program which
allows the dropping of egress traffic.
Design
We're going to:
Create a HashMap that will act as a blocklist.
Check the destination IP address from the packet against the HashMap to
make a policy decision (pass or drop).
Add entries to the blocklist from userspace.
eBPF code
The program code is going to start with a definition of BLOCKLIST map. To
enforce the policy, the program is going to lookup the destination IP address in
that map. If the map entry for that address exist, we are going to drop the
packet. Otherwise, we are going to pipe it with TC_ACT_PIPE action - which
means allowing it on our side, but let the packet be inspected also by another
Classifier programs and qdisc filters.
TC_ACT_OK
There is also a possibility to allow the packet while bypassing the other
programs and filters - TC_ACT_OK. We recommend that option only if absolutely
sure that you want your program to have a precedence over the other programs
or filters.
The purpose of the userspace code is to load the eBPF program, attach it to the
given network interface and then populate the map with an address to block.
In this example, we'll block all egress traffic going to 1.1.1.1.
usestd::net::{self,Ipv4Addr};useaya::{include_bytes_aligned,Bpf,programs::{tc,SchedClassifier,TcAttachType},maps::{perf::AsyncPerfEventArray,HashMap},util::online_cpus};usebytes::BytesMut;useclap::Parser;uselog::info;usetokio::{signal,task};usetc_egress_common::PacketLog;#[derive(Debug, Parser)]structOpt{#[clap(short, long, default_value = "eth0")]iface: String,}#[tokio::main]asyncfnmain()-> Result<(),anyhow::Error>{letopt=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)]letmutbpf=Bpf::load(include_bytes_aligned!("../../target/bpfel-unknown-none/debug/tc-egress"))?;#[cfg(not(debug_assertions))]letmutbpf=Bpf::load(include_bytes_aligned!("../../target/bpfel-unknown-none/release/tc-egress"))?;// 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);letprogram: &mutSchedClassifier=bpf.program_mut("tc_egress").unwrap().try_into()?;program.load()?;program.attach(&opt.iface,TcAttachType::Egress)?;// (1)letmutblocklist: HashMap<_,u32,u32>=HashMap::try_from(bpf.map_mut("BLOCKLIST")?)?;// (2)letblock_addr: u32=Ipv4Addr::new(1,1,1,1).try_into()?;// (3)blocklist.insert(block_addr,0,0)?;letmutperf_array=AsyncPerfEventArray::try_from(bpf.map_mut("EVENTS")?)?;forcpu_idinonline_cpus()?{letmutbuf=perf_array.open(cpu_id,None)?;task::spawn(asyncmove{letmutbuffers=(0..10).map(|_|BytesMut::with_capacity(1024)).collect::<Vec<_>>();loop{letevents=buf.read_events(&mutbuffers).await.unwrap();foriin0..events.read{letbuf=&mutbuffers[i];letptr=buf.as_ptr()as*constPacketLog;letdata=unsafe{ptr.read_unaligned()};letsrc_addr=net::Ipv4Addr::from(data.ipv4_address);info!("LOG: SRC {}, ACTION {}",src_addr,data.action);}}});}info!("Waiting for Ctrl-C...");signal::ctrl_c().await?;info!("Exiting...");Ok(())}
Loading the eBPF program.
Attaching it to the given network interface.
Populating the map with remote IP addresses which we want to prevent the
egress traffic to.
The third thing is done with getting a reference to the BLOCKLIST map and
calling blocklist.insert. Using IPv4Addr type in Rust will let us to read
the human-readable representation of IP address and convert it to u32, which
is an appropriate type to use in eBPF maps.