<buttonid="sidebar-toggle"class="icon-button"type="button"title="Toggle Table of Contents"aria-label="Toggle Table of Contents"aria-controls="sidebar">
<h1class="menu-title">Building eBPF Programs With Aya</h1>
<divclass="right-buttons">
<ahref="print.html"title="Print this book"aria-label="Print this book">
<iid="print-button"class="fa fa-print"></i>
</a>
</div>
</div>
<divid="search-wrapper"class="hidden">
<formid="searchbar-outer"class="searchbar-outer">
<inputtype="search"id="searchbar"name="searchbar"placeholder="Search this book ..."aria-controls="searchresults-outer"aria-describedby="searchresults-header">
<p>Get developers up to speed with eBPF Rust development. i.e. How to set
up a development environment.</p>
</li>
<li>
<p>Share <em>current</em> best practices about using Rust for eBPF</p>
</li>
</ul>
<h2id="who-this-book-is-for"><aclass="header"href="#who-this-book-is-for">Who This Book is For</a></h2>
<p>This book caters towards people with either some eBPF or some Rust background. For those without any prior knowledge we suggest you read the "Assumptions and Prerequisites" section first. You can check out the "Other Resources" section to find resources on topics you might want to read up on.</p>
<h3id="assumptions-and-prerequisites"><aclass="header"href="#assumptions-and-prerequisites">Assumptions and Prerequisites</a></h3>
<ul>
<li>You are comfortable using the Rust Programming Language, and have written,
run, and debugged Rust applications on a desktop environment. You should also
be familiar with the idioms of the <ahref="https://doc.rust-lang.org/edition-guide/">2018 edition</a> as this book targets
Rust 2018.</li>
</ul>
<ul>
<li>You are familiar with the core concepts of eBPF</li>
<p>If you are unfamiliar with anything mentioned above or if you want more information about a specific topic mentioned in this book you might find some of these resources helpful.</p>
<tr><td>Rust</td><td><ahref="https://doc.rust-lang.org/book/">Rust Book</a></td><td>If you are not yet comfortable with Rust, we highly suggest reading this book.</td></tr>
<tr><td>eBPF</td><td><ahref="https://docs.cilium.io/en/stable/bpf/">Cilium BPF and XDP Reference Guide</a></td><td>If you are not yet comfortable with eBPF, this guide is excellent.</td></tr>
</tbody></table>
<h2id="how-to-use-this-book"><aclass="header"href="#how-to-use-this-book">How to Use This Book</a></h2>
<p>This book generally assumes that you’re reading it front-to-back. Later
chapters build on concepts in earlier chapters, and earlier chapters may
not dig into details on a topic, revisiting the topic in a later chapter.</p>
<p>Once you have the Rust tool-chains installed, you must also install the <code>bpf-linker</code> - for linking our eBPF program - and <code>cargo-generate</code> - for generating the project skeleton.</p>
<p>While there are myriad trace points to attach to and program types to write we should start somewhere simple.</p>
<p>XDP (eXpress Data Path) programs permit our eBPF program to make decisions about packets that have been received on the interface to which our program is attached. To keep things simple, we'll build a very simplistic firewall to permit or deny traffic.</p>
<li><code>#[xdp]</code> indicates that this function is an XDP program</li>
<li>The <code>try_xdp_firewall</code> function returns a Result that permits all traffic</li>
<li>The <code>xdp_firewall</code> program calls <code>try_xdp_firewall</code> and handles any errors by returning <code>XDP_ABORTED</code>, which will drop the packet and raise a tracepoint exception.</li>
</ul>
<p>Now we can compile this using <code>cargo xtask build-ebpf</code></p>
<h3id="verifying-the-program"><aclass="header"href="#verifying-the-program">Verifying The Program</a></h3>
<p>Let's take a look at the compiled eBPF program:</p>
<p>We will add a dependency on <code>ctrlc = "3.2"</code> to <code>myapp/Cargo.toml</code>, then add the following imports at the top of the <code>myapp/src/main.rs</code>:</p>
<li>The interface we wish to attach it to (defaults to <code>eth0</code>)</li>
</ul>
<p>The line <code>let mut bpf = Bpf::load_file(&path)?;</code>:</p>
<ul>
<li>Opens the file</li>
<li>Reads the ELF contents</li>
<li>Creates any maps</li>
<li>If your system supports BPF Type Format (BTF), it will read the current BTF description and performs any necessary relocations</li>
</ul>
<p>Once our file is loaded, we can extract the XDP probe with <code>let probe: &mut Xdp = bpf.program_mut("xdp")?.try_into()?;</code> and then load it in to the kernel with <code>probe.load()</code>.</p>
<p>Finally, we can attach it to an interface with <code>probe.attach(&iface, XdpFlags::default())?;</code></p>
<p>In the previous chapter, our XDP application ran for 10 seconds and permitted some traffic.
There was however no output on the console, so you just have to trust that it was working correctly. Let's expand this program to log the traffic that is being permitted</p>
<h2id="getting-data-to-user-space"><aclass="header"href="#getting-data-to-user-space">Getting Data to User-Space</a></h2>
<p>To get data from kernel-space to user-space we use an eBPF map. There are numerous types of maps to chose from, but in this example we'll be using a PerfEventArray.</p>
<p>While we could go all out and extract data all the way up to L7, we'll constrain our firewall to L3, and to make things easier, IPv4 only.
The data structure that we'll need to send information to user-space will need to hold an IPv4 address and an action for Permit/Deny, we'll encode both as a <code>u32</code>.</p>
<p>Let's go ahead and add that to <code>myapp-common/src/lib.rs</code></p>
<pre><codeclass="language-rust ignore">#[repr(C)]
pub struct PacketLog {
pub ipv4_address: u32,
pub action: u32,
}
#[cfg(feature = "user")]
unsafe impl aya::Pod for PacketLog {}
</code></pre>
<blockquote>
<p>💡 <strong>HINT: Struct Alignment</strong></p>
<p>Structs must be aligned to 8 byte boundaries. You can do this manually, or alternatively you may use <code>#[repr(packed)]</code>. If you do not do this, the eBPF verifier will get upset and emit an <code>invalid indirect read from stack</code> error.</p>
</blockquote>
<p>We implement the <code>aya::Pod</code> trait for our struct since it is Plain Old Data as can be safely converted to a byte-slice and back.</p>
<p>Now we've got our maps set up, let's add some data!</p>
<h3id="generating-bindings-to-vmlinuxh"><aclass="header"href="#generating-bindings-to-vmlinuxh">Generating Bindings To vmlinux.h</a></h3>
<p>To get useful data to add to our maps, we first need some useful data structures to populate with data from the <code>XdpContext</code>.
We want to log the Source IP Address of incoming traffic, so we'll need to:</p>
<ol>
<li>Read the Ethernet Header to determine if this is an IPv4 Packet</li>
<li>Read the Source IP Address from the IPv4 Header</li>
</ol>
<p>The two structs in the kernel for this are <code>ethhdr</code> from <code>uapi/linux/if_ether.h</code> and <code>iphdr</code> from <code>uapi/linux/ip.h</code>.
If I were to use bindgen to generate Rust bindings for those headers, I'd be tied to the kernel version of the system that I'm developing on.
This is where <code>aya-gen</code> comes in to play. It can easily generate bindings for using the BTF information in <code>/sys/kernel/btf/vmlinux</code>.</p>
<p>Once the bindings are generated and checked in to our repository they shouldn't need to be regenerated again unless we need to add a new struct.</p>
<p>Lets use <code>xtask</code> to automate this so we can easily reproduce this file in future.</p>
<p>We'll add the following content to <code>xtask/src/codegen.rs</code></p>
let dir = PathBuf::from("myapp-ebpf/src");
let names: Vec<&str> = vec!["ethhdr", "iphdr"];
let bindings = btf_types::generate(Path::new("/sys/kernel/btf/vmlinux"), &names, false)?;
// Write the bindings to the $OUT_DIR/bindings.rs file.
let mut out = File::create(dir.join("bindings.rs"))?;
write!(out, "{}", bindings).expect("unable to write bindings to file");
Ok(())
}
</code></pre>
<p>This will generate a file called <code>myapp-ebpf/src/bindings.rs</code>. If you've chosen an application name other than <code>myapp</code> you'll need to adjust the path appropriately.</p>
<p>Add a new dependencies to <code>xtask/Cargo.toml</code>:</p>
<h3id="getting-packet-data-from-the-context"><aclass="header"href="#getting-packet-data-from-the-context">Getting Packet Data From The Context</a></h3>
<p>The <code>XdpContext</code> contains two fields, <code>data</code> and <code>data_end</code>.
<code>data</code> is a pointer to the start of the data in kernel memory and <code>data_end</code>, a pointer to the end of the data in kernel memory. In order to access this data and ensure that the eBPF verifier is happy, we'll introduce a helper function:</p>
<p>This function will ensure that before we access any data, we check that it's contained between <code>data</code> and <code>data_end</code>.
It is marked as <code>unsafe</code> because when calling the function, you must ensure that there is a valid <code>T</code> at that location or there will be undefined behaviour.</p>
<h3id="writing-data-to-the-map"><aclass="header"href="#writing-data-to-the-map">Writing Data To The Map</a></h3>
<p>With our helper function in place, we can:</p>
<ol>
<li>Read the Ethertype field to check if we have an IPv4 packet.</li>
<li>Read the IPv4 Source Address from the IP header</li>
</ol>
<p>First let's add another dependency on <code>memoffset = "0.6"</code> to <code>myapp-ebpf/Cargo.toml</code>, and then we'll change our <code>try_xdp_firewall</code> function to look like this:</p>
<p>In order to read from the <code>AsyncPerfEventArray</code>, we have to call <code>AsyncPerfEventArray::open()</code> for each online CPU, then we have to poll the file descriptor for events.
While this is do-able using <code>PerfEventArray</code> and <code>mio</code> or <code>epoll</code>, the code is much less easy to follow. Instead, we'll use <code>tokio</code> to make our user-space application async.</p>
<p>Let's add some dependencies to <code>myapp/src/Cargo.toml</code>:</p>
<pre><codeclass="language-toml">[dependencies]
aya = { git = "https://github.com/alessandrod/aya", branch="main", features=["async_tokio"] }
signal::ctrl_c().await.expect("failed to listen for event");
Ok::<_, anyhow::Error>(())
}
</code></pre>
<p>This will now spawn a <code>tokio::task</code> to read each of the <code>AsyncPerfEventArrayBuffers</code> contained in out <code>AsyncPerfEventArray</code>.
When we receive an event, we use <code>read_unaligned</code> to read our data into a <code>PacketLog</code>.
We then use <code>println!</code> to log the event to the console.
We no longer need to sleep, as we run until we receive the <code>CTRL+C</code> signal.</p>
<h2id="running-the-program"><aclass="header"href="#running-the-program">Running the program</a></h2>