previously we learned the basics of Ebpf and How to use the BPF library for the Rust programming language here will see eBPF program attached to sys_enter_exeve
tracepoint in the Linux kernel and is executed for each sys_execve
syscall.
Unsafe Rust
for eBPF programs written using rust, it is often required to access Linux kernel data structure or call Linux kernel helper functions. Unsafe Rust is the glue that helps achieve this functionality
following are the two essential functionalities enabled by unsafe Rust concerning eBPF programs
ability to dereference a raw memory pointer that points to Linux kernel data structure
ability to call a Linux kernel helper function implemented in C
Unsafe code in Rust is enclosed in a code block or a function block marked with an unsafe keyword the best way to understand how unsafe rust enables an eBPF program to interface with the Linux kernel is via the following two simple programs
Kernel Data Structure Access
every eBPF program gets calked with context passes as an input parameter. The context is the kernel data structure pointer, its type depends on the eBPF program type. the memory referenced by the context can access only with unsafe Rust code
unsafe fn ptr_at<T>(ctx: &Xdpcontext,offset: unsize)
-> 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 + offser) as *const T)
}
In the code snippet above, the ptr_at
the function used by an eBPF program of the type BPF_PROG_TYPE_XDP
the function takes ctx
as an input parameter, a reference to an object type XdpContext
this reference represents a C
struct variable of type struct xdp_md
in the Linux kernel. The xdp_md
data structure points to a region in memory where a network packet resides. the start
and end
variables store the starting and ending memory address for the packet, while offset
represent an offset within the packet. ptr_at
the function returns a pointer to a valid offset within the packet's memory representation
fn try_xdp_firewall(ctx: XdpContext) ->
Result<u32, () > {
let eth_type = u16::from_be(unsafe {
*ptr_at(&ctx, offset_of!(ethhdr,h_proto))?
});
if eth_type != ETH_P_IP {
return Ok(XdpContext::XDP_PASS);
}
let source = u32::from_be(unsafe {
*ptr_at(&ctx, ETH_HDR_LEN + offset_of!(iphdr,saddr))?
});
if blocked_ip(source){
return Ok(xdp_action::XDP_DROP)
}
Ok(xdp_action::XDP_PASS)
}
the try_xdp_firewall
uses the ptr_at
function to get the EtherType
from the raw packet. if the packet encapsulated in the ethernet frame is an IP packet, the source IP is extracted using ptr-at
function. if the source IP address is blocked the packet is dropped by return XDP_DROP
action.
kernel Function call
unsafe rust code also enables the calling of certain Linux kernel helper function from the eBPF programs. The kernel helper functions that the eBPF program is permitted to call are contingent on the type of the program
use aya_bpf::{
programs::ProbeContext,
helpers::bpf_ktime_get_ns,
};
fn try_nfs_file_read(ctx:ProbeContext)
- > Result<u32,u32> {
let start_mount_ns =
unsafe { bpf_ktime_get_ns()};
ok(0)
}
In the code snippet above, bpf_ktime_get_ns
is a Linux kernel helper function that returns the number of nanoseconds since boot time and is used to compute the time delta between events or as a timestamp in this example the bpf_ktime_get_ns
the function is called to record the start time of the NFS file read encapsulating the helper function call in an unsafe block.
let's install lima with ubuntu
limactl start ubuntu-lts
INFO[0000] Creating an instance "ubuntu-lts" from template://default (Not from template://ubuntu-lts)
WARN[0000] This form is deprecated. Use `limactl start --name=ubuntu-lts template://default` instead
? Creating an instance "ubuntu-lts" Proceed with the current configuration
INFO[0017] Attempting to download the image from "https://cloud-images.ubuntu.com/releases/22.10/release-20221201/ubuntu-22.10-server-cloudimg-arm64.img" digest="sha256:9575dfe9f925ec251a933b88a38c5582a18e9d19495025ac01cb2e217e5f14ca"
47.94 MiB / 664.62 MiB [-->___________________________________] 7.21% 3.52 MiB/s
limactl shell ubuntu-lts
connect Lima to your Vscode
limactl show-ssh --format=config ubuntu-lts
Host lima-ubuntu-lts
IdentityFile "/Users/sangambiradar/.lima/_config/user"
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
NoHostAuthenticationForLocalhost yes
GSSAPIAuthentication no
PreferredAuthentications publickey
Compression no
BatchMode yes
IdentitiesOnly yes
Ciphers "^aes128-gcm@openssh.com,aes256-gcm@openssh.com"
User sangambiradar
ControlMaster auto
ControlPath "/Users/sangambiradar/.lima/ubuntu-lts/ssh.sock"
ControlPersist 5m
Hostname 127.0.0.1
Port 55305
we will see Trace Programs
Kernel Proble(kprobe)
kernel Tracepoint
User Space Probe ( UProbe)
User Space Tracepoint
Kernel Probe ( Kprobe)
Linux kernel probes enable you to attach eBPF programs to kernel functions to gather information or modify the functionβs behaviour. This section will provide examples of both.
$ cat /proc/kallsyms
There are some 300+ different system calls in the Linux kernel. Applications/workloads running in the user space interact with the kernel using syscalls. e.g. if an application wants to do something like access a file, communicate using a network or even find the time of day, it will have to ask the kernel to do it on the application's behalf via a syscall; hence by attaching these probes to syscalls we're able to judiciously monitor what applications are doing in the user space. A list of all kernel syscall identifiers for a kernel can be found in this file: /proc/kallsyms
. Here is an excerpt:
0000000000000000 T vs_create
0000000000000000 T vfs_mkdir
0000000000000000 T vfs_mknod
0000000000000000 T vs link
0000000000000000 T vs unlink
0000000000000000 t path_openat
0000000000000000 T vs_rename
0000000000000000 T getname kernel
0000000000000000 T putname
0000000000000000 t getname_flags.part. O
0000000000000000 T getname flags
0000000000000000 T getname
System.map file and /proc/kallsyms
System.map file format: address type notation
The type is lowercase for local symbols, and uppercase for global (external).
Focus on several types:
T, The symbol is in the text(code) section
D The symbol is in the initialized data section
R The symbol is in a read-only data section
t static
d static
R const
r static const
For example, suppose you have a Rust object file named hello.o
that contains a function named hello
. You can use nm
to list the symbols in this object file by running the following command:
$ nm hello.o
This will print a list of symbols in the hello.o
object file, which might look something like this:
0000000000000000 T hello
U __libc_start_main
Block Mount
start of the open_ctree
function call. The eBPF helper function bpf_override_return
short circuits the open_ctree function execution by returning ENOMEM. This prevents the btrfs volume from being mounted.
#![no_std]
#![no_main]
use aya_bpf::{
helpers::bpf_override_return, macros::kprobe,
programs::ProbeContext,
};
use aya_log_ebpf::info;
const ENOMEM: i32 = -12;
#[kprobe(name = "block_mount")]
pub fn block_mount(ctx: ProbeContext) -> u32 {
match try_block_mount(ctx) {
Ok(ret) => ret,
Err(ret) => ret,
}
}
fn try_block_mount(
ctx: ProbeContext,
) -> Result<u32, u32> {
info!(&ctx, "function open_ctree called");
unsafe {
bpf_override_return(ctx.regs, ENOMEM as u64)
};
Ok(0)
}
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
unsafe { core::hint::unreachable_unchecked() }
}
kernel Tracepoint
cargo generate https://github.com/aya-rs/aya-template
π€· Project Name: tracepoint-sangam
π§ Destination: /home/ubuntu/tracepoint-sangam ...
π§ project-name: tracepoint-sangam ...
π§ Generating template ...
β π€· Which type of eBPF program? Β· tracepoint
π€· Which tracepoint category? (e.g sched, net etc...): sys_enter_execve
π€· Which tracepoint name? (e.g sched_switch, net_dev_queue): systemcalls
[ 1/35] Done: .cargo/config.toml [ 2/35] Done: .cargo [ 3/35] Done: .gitignore [ 4/35] Done: .vim/coc-settings.json [ 5/35] Done: .vim [ 6/35] Done: .vscode/settings.json [ 7/35] Done: .vscode [ 8/35] Done: Cargo.toml [ 9/35] Done: README.md [10/35] Ignored: pre-script.rhai [11/35] Done: xtask/Cargo.toml [12/35] Done: xtask/src/build_ebpf.rs [13/35] Done: xtask/src/main.rs [14/35] Done: xtask/src/run.rs [15/35] Done: xtask/src [16/35] Done: xtask [17/35] Done: tracepoint-sangam/Cargo.toml [18/35] Done: tracepoint-sangam/src/main.rs [19/35] Done: tracepoint-sangam/src [20/35] Done: tracepoint-sangam [21/35] Done: tracepoint-sangam-common/Cargo.toml [22/35] Done: tracepoint-sangam-common/src/lib.rs [23/35] Done: tracepoint-sangam-common/src [24/35] Done: tracepoint-sangam-common [25/35] Done: tracepoint-sangam-ebpf/.cargo/config.toml [26/35] Done: tracepoint-sangam-ebpf/.cargo [27/35] Done: tracepoint-sangam-ebpf/.vim/coc-settings.json [28/35] Done: tracepoint-sangam-ebpf/.vim [29/35] Done: tracepoint-sangam-ebpf/.vscode/settings.json [30/35] Done: tracepoint-sangam-ebpf/.vscode [31/35] Done: tracepoint-sangam-ebpf/Cargo.toml [32/35] Done: tracepoint-sangam-ebpf/rust-toolchain.toml [33/35] Done: tracepoint-sangam-ebpf/src/main.rs [34/35] Done: tracepoint-sangam-ebpf/src [35/35] Done: tracepoint-sangam-ebpf π§ Moving generated files into: `/home/ubuntu/tracepoint-sangam`...
π‘ Initializing a fresh Git repository
β¨ Done! New project created /home/ubuntu/tracepoint-sangam
ubuntu@ip-172-31-56-217:~$
its generate template
.
βββ Cargo.toml
βββ README.md
βββ tracepoint-sangam
β βββ Cargo.toml
β βββ src
β βββ main.rs
βββ tracepoint-sangam-common
β βββ Cargo.toml
β βββ src
β βββ lib.rs
βββ tracepoint-sangam-ebpf
β βββ Cargo.toml
β βββ rust-toolchain.toml
β βββ src
β βββ main.rs
βββ xtask
βββ Cargo.toml
βββ src
βββ build_ebpf.rs
βββ main.rs
βββ run.rs
tracepoint-sangam - contains the user space program
tracepoint-sangam-ebpf - contains the epbf program
trace-sangam-common contains the code for data structure and data types common to both user space and eBPF programs
The user space program can be buit using
~/tracepoint-sangam$ cargo build
the eBPF program can be built using
~/tracepoint-sangam$ cargo xtask build-ebpf
the release version of the eBPF program can be build using
~/tracepoint-sangam$ cargo xtask build-ebpf --release
The userspace program which loads the eBPF program into kernel can be run using
/tracepoint-sangam$ cargo xtask run
above simple ebpf program attached to sys_enter_exeve
tracepoint in the Linux kenel and is executed for each sys_execve
syscall.
The eBPF program on execution logs a perf event and this event is returned to the user space for printing via the perf event array map. exporting the logs from the kernel to the user space is abstracted from the developer and handled by the aya-logging library
That's it for this part we will learn more about aya and ebpf .
the eBPF developer experience by allowing Rust programs to easily run within the kernel.
Aya is the first Rust-native eBPF library that is similar in nature to libbpf but entirely written in the Rust programming language, popular for its memory safety and concurrency features, among other reasons this programming language is becoming very popular for systems programming.
ThreatStryker Sandbox - https://deepfence.io/view-enterprise-sandbox/
this blog is sponsored by deepfence.io
Take Deepfenceβs Cloud-Native Application Protection Platform for a spin