Probes
Source Code
Full code for the example in this chapter is available here.
What are the probes in eBPF?
The probe BPF programs attach to kernel (kprobes) or user-side (uprobes) functions and are able to access the function parameters of those functions. You can find more information about probes in the kernel documentation, including the difference between kprobes and kretprobes.
Example project
To illustrate kprobes with Aya, let's write a program which
attaches a eBPF handler to the tcp_connect
function and allows
printing the source and destination IP addresses from the socket parameter.
Design
For this demo program, we are going to rely on aya-log to print IP addresses from the BPF program and not going to have any custom BPF maps (besides those created by aya-log).
eBPF code
- From the
tcp_connect
signature, we see thatstruct sock *sk
is the only function parameter. We will access it from theProbeContext
ctx handle. - We call
bpf_probe_read_kernel
helper to copy thestruct sock_common __sk_common
portion of the socket structure. (For uprobe programs, we would need to callbpf_probe_read_user
instead.) - We match the
skc_family
field, and forAF_INET
(IPv4) andAF_INET6
(IPv6) values, extract and print the src and destination addresses using aya-loginfo!
macro.
Here's how the eBPF code looks like:
```rust linenums="1" title="kprobetcp-ebpf/src/main.rs"
#![no_std]
#![no_main]
#[allow(non_upper_case_globals)]
#[allow(non_snake_case)]
#[allow(non_camel_case_types)]
#[allow(dead_code)]
mod binding;
use crate::binding::{sock, sock_common};
use aya_ebpf::{
helpers::bpf_probe_read_kernel, macros::kprobe, programs::ProbeContext,
};
use aya_log_ebpf::info;
const AF_INET: u16 = 2;
const AF_INET6: u16 = 10;
#[kprobe]
pub fn kprobetcp(ctx: ProbeContext) -> u32 {
match try_kprobetcp(ctx) {
Ok(ret) => ret,
Err(ret) => match ret.try_into() {
Ok(rt) => rt,
Err(_) => 1,
},
}
}
fn try_kprobetcp(ctx: ProbeContext) -> Result<u32, i64> {
let sock: *mut sock = ctx.arg(0).ok_or(1i64)?;
let sk_common = unsafe {
bpf_probe_read_kernel(&(*sock).__sk_common as *const sock_common)
.map_err(|e| e)?
};
match sk_common.skc_family {
AF_INET => {
let src_addr = u32::from_be(unsafe {
sk_common.__bindgen_anon_1.__bindgen_anon_1.skc_rcv_saddr
});
let dest_addr: u32 = u32::from_be(unsafe {
sk_common.__bindgen_anon_1.__bindgen_anon_1.skc_daddr
});
info!(
&ctx,
"AF_INET src address: {:i}, dest address: {:i}",
src_addr,
dest_addr,
);
Ok(0)
}
AF_INET6 => {
let src_addr = sk_common.skc_v6_rcv_saddr;
let dest_addr = sk_common.skc_v6_daddr;
info!(
&ctx,
"AF_INET6 src addr: {:i}, dest addr: {:i}",
unsafe { src_addr.in6_u.u6_addr8 },
unsafe { dest_addr.in6_u.u6_addr8 }
);
Ok(0)
}
_ => Ok(0),
}
}
#[cfg(not(test))]
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
loop {}
}
```
Userspace code
The purpose of the userspace code is to load the eBPF program and attach it to the
tcp_connect
function.
Here's how the code looks like:
```rust linenums="1" title="kprobetcp/src/main.rs"
use aya::{include_bytes_aligned, programs::KProbe, Ebpf};
use aya_log::EbpfLogger;
use clap::Parser;
use log::{info, warn};
use tokio::signal;
#[derive(Debug, Parser)]
struct Opt {}
#[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 `Ebpf::load_file` instead.
#[cfg(debug_assertions)]
let mut bpf = Ebpf::load(include_bytes_aligned!(
"../../target/bpfel-unknown-none/debug/kprobetcp"
))?;
#[cfg(not(debug_assertions))]
let mut bpf = Ebpf::load(include_bytes_aligned!(
"../../target/bpfel-unknown-none/release/kprobetcp"
))?;
if let Err(e) = EbpfLogger::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 KProbe =
bpf.program_mut("kprobetcp").unwrap().try_into()?;
program.load()?;
program.attach("tcp_connect", 0)?;
info!("Waiting for Ctrl-C...");
signal::ctrl_c().await?;
info!("Exiting...");
Ok(())
}
```
Running the program
```console
$ RUST_LOG=info cargo xtask run --release
[2022-12-28T20:50:00Z INFO kprobetcp] Waiting for Ctrl-C...
[2022-12-28T20:50:05Z INFO kprobetcp] AF_INET6 src addr: 2001:4998:efeb:282::249, dest addr: 2606:2800:220:1:248:1893:25c8:1946
[2022-12-28T20:50:11Z INFO kprobetcp] AF_INET src address: 10.53.149.148, dest address: 10.87.116.72
[2022-12-28T20:50:30Z INFO kprobetcp] AF_INET src address: 10.53.149.148, dest address: 98.138.219.201
```