diff --git a/.gitignore b/.gitignore index e4b5e9db..7b0db703 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,9 @@ compile_commands.json *.ll *.o *.skel.h + +# Rust +**/debug/ +**/target/ +**/Cargo.lock +**/*.rs.bk \ No newline at end of file diff --git a/README.md b/README.md index 243acf5a..f98ba885 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ This git repository contains a diverse set of **practical BPF examples** that solve (or demonstrate) a specific use-case using BPF. It is meant to ease doing **rapid prototyping and development**, writing C-code -BPF programs using libbpf. The goal is to make it **easier for developers** to -get started coding. +BPF programs using libbpf, or Rust BPF programs using Aya. +The goal is to make it **easier for developers** to get started coding. Many developers struggle to get a working BPF build environment. The repo enviroment makes it easy to build/compile BPF programs by doing the necessary diff --git a/echo/.cargo/config.toml b/echo/.cargo/config.toml new file mode 100644 index 00000000..f0ccbc9a --- /dev/null +++ b/echo/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +xtask = "run --package xtask --" \ No newline at end of file diff --git a/echo/.vim/coc-settings.json b/echo/.vim/coc-settings.json new file mode 100644 index 00000000..52a6f0d1 --- /dev/null +++ b/echo/.vim/coc-settings.json @@ -0,0 +1,3 @@ +{ + "rust-analyzer.linkedProjects": ["Cargo.toml", "echo-ebpf/Cargo.toml"] +} diff --git a/echo/.vscode/settings.json b/echo/.vscode/settings.json new file mode 100644 index 00000000..52a6f0d1 --- /dev/null +++ b/echo/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "rust-analyzer.linkedProjects": ["Cargo.toml", "echo-ebpf/Cargo.toml"] +} diff --git a/echo/Cargo.toml b/echo/Cargo.toml new file mode 100644 index 00000000..9139ef05 --- /dev/null +++ b/echo/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["echo", "echo-tokio", "xtask"] diff --git a/echo/README.md b/echo/README.md new file mode 100644 index 00000000..3b5192d9 --- /dev/null +++ b/echo/README.md @@ -0,0 +1,32 @@ +# echo + +Echo is an example that creates a TCP Echo Server using socket redirection. +For comparison, a simple (naive) TCP Echo Server is also provided. + + +## Prerequisites + +1. Install a rust stable toolchain: `rustup install stable` +1. Install a rust nightly toolchain: `rustup install nightly` +1. Install bpf-linker: `cargo install bpf-linker` + +## Running The eBPF Example + +```bash +cargo xtask build-ebpf +cargo xtask run +``` + +## Running the Native Example + +```bash +cargo run --bin echo-tokio +``` + +## Interacting with the Example + +```bash +nc localhost 41234 +``` + +Any data sent will be echoed back to the client \ No newline at end of file diff --git a/echo/echo-ebpf/.cargo/config.toml b/echo/echo-ebpf/.cargo/config.toml new file mode 100644 index 00000000..5d7e5915 --- /dev/null +++ b/echo/echo-ebpf/.cargo/config.toml @@ -0,0 +1,6 @@ +[build] +target-dir = "../target" +target = "bpfel-unknown-none" + +[unstable] +build-std = ["core"] \ No newline at end of file diff --git a/echo/echo-ebpf/.vim/coc-settings.json b/echo/echo-ebpf/.vim/coc-settings.json new file mode 100644 index 00000000..e2211a64 --- /dev/null +++ b/echo/echo-ebpf/.vim/coc-settings.json @@ -0,0 +1,4 @@ +{ + "rust-analyzer.cargo.target": "bpfel-unknown-none", + "rust-analyzer.checkOnSave.allTargets": false +} diff --git a/echo/echo-ebpf/.vscode/settings.json b/echo/echo-ebpf/.vscode/settings.json new file mode 100644 index 00000000..b5783ae7 --- /dev/null +++ b/echo/echo-ebpf/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "rust-analyzer.cargo.target": "bpfel-unknown-none", + "rust-analyzer.checkOnSave.allTargets": false, +} \ No newline at end of file diff --git a/echo/echo-ebpf/Cargo.toml b/echo/echo-ebpf/Cargo.toml new file mode 100644 index 00000000..c66aa85a --- /dev/null +++ b/echo/echo-ebpf/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "echo-ebpf" +version = "0.1.0" +edition = "2018" + +[dependencies] +aya-bpf = { git = "https://github.com/aya-rs/aya", branch="main" } + +[[bin]] +name = "echo" +path = "src/main.rs" + +[profile.dev] +panic = "abort" +debug = 1 +opt-level = 2 +overflow-checks = false + +[profile.release] +panic = "abort" + +[workspace] +members = [] \ No newline at end of file diff --git a/echo/echo-ebpf/rust-toolchain.toml b/echo/echo-ebpf/rust-toolchain.toml new file mode 100644 index 00000000..c046a094 --- /dev/null +++ b/echo/echo-ebpf/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel="nightly" diff --git a/echo/echo-ebpf/src/main.rs b/echo/echo-ebpf/src/main.rs new file mode 100644 index 00000000..6062352d --- /dev/null +++ b/echo/echo-ebpf/src/main.rs @@ -0,0 +1,45 @@ +#![no_std] +#![no_main] + +use aya_bpf::{ + bindings::sk_action, + macros::{map, stream_parser, stream_verdict}, + maps::SockMap, + programs::SkBuffContext, +}; + +#[map(name = "sockmap")] +static mut SOCKMAP: SockMap = SockMap::with_max_entries(1, 0); + +#[stream_parser] +fn stream_parser(ctx: SkBuffContext) -> u32 { + match { try_stream_parser(ctx) } { + Ok(ret) => ret, + Err(ret) => ret, + } +} + +fn try_stream_parser(ctx: SkBuffContext) -> Result { + Ok(ctx.len()) +} + +#[stream_verdict] +fn stream_verdict(ctx: SkBuffContext) -> u32 { + match unsafe { try_stream_verdict(ctx) } { + Ok(_) => sk_action::SK_PASS, + Err(_) => sk_action::SK_DROP, + } +} + +unsafe fn try_stream_verdict(ctx: SkBuffContext) -> Result { + match SOCKMAP.redirect_skb(&ctx, 0, 0) as u32 { + sk_action::SK_PASS => Ok(sk_action::SK_PASS), + sk_action::SK_DROP => Err(sk_action::SK_DROP), + _ => Err(sk_action::SK_DROP), + } +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + unsafe { core::hint::unreachable_unchecked() } +} diff --git a/echo/echo-tokio/Cargo.toml b/echo/echo-tokio/Cargo.toml new file mode 100644 index 00000000..71444846 --- /dev/null +++ b/echo/echo-tokio/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "echo-tokio" +version = "0.1.0" +edition = "2018" + +[dependencies] +tokio = { version = "1.9.0", features = ["full"] } \ No newline at end of file diff --git a/echo/echo-tokio/src/main.rs b/echo/echo-tokio/src/main.rs new file mode 100644 index 00000000..a237d1f2 --- /dev/null +++ b/echo/echo-tokio/src/main.rs @@ -0,0 +1,34 @@ +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::TcpListener; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let listener = TcpListener::bind("127.0.0.1:41234").await?; + + loop { + let (mut socket, _) = listener.accept().await?; + + tokio::spawn(async move { + let mut buf = [0; 65535]; + + // In a loop, read data from the socket and write the data back. + loop { + let n = match socket.read(&mut buf).await { + // socket closed + Ok(n) if n == 0 => return, + Ok(n) => n, + Err(e) => { + eprintln!("failed to read from socket; err = {:?}", e); + return; + } + }; + + // Write the data back + if let Err(e) = socket.write_all(&buf[0..n]).await { + eprintln!("failed to write to socket; err = {:?}", e); + return; + } + } + }); + } +} diff --git a/echo/echo/Cargo.toml b/echo/echo/Cargo.toml new file mode 100644 index 00000000..97d395ab --- /dev/null +++ b/echo/echo/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "echo" +version = "0.1.0" +edition = "2018" +publish = false + +[dependencies] +aya = { git = "https://github.com/aya-rs/aya", branch="main" } +anyhow = "1.0.42" +ctrlc = "3.2" +structopt = { version = "0.3"} +tokio = { version = "1.9.0", features = ["full"] } + +[[bin]] +name = "echo" +path = "src/main.rs" diff --git a/echo/echo/src/main.rs b/echo/echo/src/main.rs new file mode 100644 index 00000000..7baca361 --- /dev/null +++ b/echo/echo/src/main.rs @@ -0,0 +1,46 @@ +use aya::maps::{MapRefMut, SockMap}; +use aya::programs::SkSkb; +use aya::{include_bytes_aligned, Bpf}; +use tokio::io::AsyncReadExt; +use tokio::signal; + +use std::convert::{TryFrom, TryInto}; + +use tokio::net::TcpListener; + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + #[cfg(debug_assertions)] + let mut bpf = Bpf::load(include_bytes_aligned!( + "../../target/bpfel-unknown-none/debug/echo" + ))?; + #[cfg(not(debug_assertions))] + let mut bpf = Bpf::load(include_bytes_aligned!( + "../../target/bpfel-unknown-none/release/echo" + ))?; + let mut sock_map = SockMap::::try_from(bpf.map_mut("sockmap")?)?; + + let parser: &mut SkSkb = bpf.program_mut("stream_parser")?.try_into()?; + parser.load()?; + parser.attach(&sock_map)?; + + let verdict: &mut SkSkb = bpf.program_mut("stream_verdict")?.try_into()?; + verdict.load()?; + verdict.attach(&sock_map)?; + + let listener = TcpListener::bind("127.0.0.1:41234").await?; + + println!("Server Listening on {}", listener.local_addr().unwrap()); + // TODO: currently this will only accept one connection at a time. add up to map max_entries handlers + tokio::spawn(async move { + loop { + let (mut socket, _) = listener.accept().await.unwrap(); + sock_map.set(0, &socket, 0).unwrap(); + let mut buf = [0; 0]; + socket.read(&mut buf[..]).await.unwrap(); + sock_map.clear_index(&0).unwrap(); + } + }); + signal::ctrl_c().await?; + Ok(()) +} diff --git a/echo/xtask/Cargo.toml b/echo/xtask/Cargo.toml new file mode 100644 index 00000000..3b50084b --- /dev/null +++ b/echo/xtask/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "xtask" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +structopt = {version = "0.3", default-features = false } +anyhow = "1" \ No newline at end of file diff --git a/echo/xtask/src/build_ebpf.rs b/echo/xtask/src/build_ebpf.rs new file mode 100644 index 00000000..aa2953f5 --- /dev/null +++ b/echo/xtask/src/build_ebpf.rs @@ -0,0 +1,64 @@ +use std::path::PathBuf; +use std::process::Command; + +use structopt::StructOpt; + +#[derive(Debug, Copy, Clone)] +pub enum Architecture { + BpfEl, + BpfEb, +} + +impl std::str::FromStr for Architecture { + type Err = String; + + fn from_str(s: &str) -> Result { + Ok(match s { + "bpfel-unknown-none" => Architecture::BpfEl, + "bpfeb-unknown-none" => Architecture::BpfEb, + _ => return Err("invalid target".to_owned()), + }) + } +} + +impl std::fmt::Display for Architecture { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Architecture::BpfEl => "bpfel-unknown-none", + Architecture::BpfEb => "bpfeb-unknown-none", + }) + } +} + +#[derive(StructOpt)] +pub struct Options { + /// Set the endianness of the BPF target + #[structopt(default_value = "bpfel-unknown-none", long)] + pub target: Architecture, + /// Build the release target + #[structopt(long)] + pub release: bool, +} + +pub fn build_ebpf(opts: Options) -> Result<(), anyhow::Error> { + let dir = PathBuf::from("echo-ebpf"); + let target = format!("--target={}", opts.target); + let mut args = vec![ + "+nightly", + "build", + "--verbose", + target.as_str(), + "-Z", + "build-std=core", + ]; + if opts.release { + args.push("--release") + } + let status = Command::new("cargo") + .current_dir(&dir) + .args(&args) + .status() + .expect("failed to build bpf program"); + assert!(status.success()); + Ok(()) +} diff --git a/echo/xtask/src/main.rs b/echo/xtask/src/main.rs new file mode 100644 index 00000000..9dedae1d --- /dev/null +++ b/echo/xtask/src/main.rs @@ -0,0 +1,32 @@ +mod build_ebpf; +mod run; + +use std::process::exit; + +use structopt::StructOpt; +#[derive(StructOpt)] +pub struct Options { + #[structopt(subcommand)] + command: Command, +} + +#[derive(StructOpt)] +enum Command { + BuildEbpf(build_ebpf::Options), + Run(run::Options), +} + +fn main() { + let opts = Options::from_args(); + + use Command::*; + let ret = match opts.command { + BuildEbpf(opts) => build_ebpf::build_ebpf(opts), + Run(opts) => run::run(opts), + }; + + if let Err(e) = ret { + eprintln!("{:#}", e); + exit(1); + } +} diff --git a/echo/xtask/src/run.rs b/echo/xtask/src/run.rs new file mode 100644 index 00000000..9bad34e0 --- /dev/null +++ b/echo/xtask/src/run.rs @@ -0,0 +1,75 @@ +use std::{os::unix::process::CommandExt, process::Command}; + +use anyhow::Context as _; +use structopt::StructOpt; + +use crate::build_ebpf::{build_ebpf, Architecture, Options as BuildOptions}; + +#[derive(StructOpt)] +pub struct Options { + /// Set the endianness of the BPF target + #[structopt(default_value = "bpfel-unknown-none", long)] + pub bpf_target: Architecture, + /// Build and run the release target + #[structopt(long)] + pub release: bool, + /// The command used to wrap your application + #[structopt(short, long, default_value = "sudo -E")] + pub runner: String, + /// A convenience flag that will supply `--path /path/to/bpf/object` to your application + #[structopt(short = "p", long)] + pub supply_path: bool, + /// Arguments to pass to your application + #[structopt(name = "args", last = true)] + pub run_args: Vec, +} + +/// Build the project +fn build(opts: &Options) -> Result<(), anyhow::Error> { + let mut args = vec!["build"]; + if opts.release { + args.push("--release") + } + let status = Command::new("cargo") + .args(&args) + .status() + .expect("failed to build userspace"); + assert!(status.success()); + Ok(()) +} + +/// Build and run the project +pub fn run(opts: Options) -> Result<(), anyhow::Error> { + // build our ebpf program followed by our application + build_ebpf(BuildOptions { + target: opts.bpf_target, + release: opts.release, + }) + .context("Error while building eBPF program")?; + build(&opts).context("Error while building userspace application")?; + + // profile we are building (release or debug) + let profile = if opts.release { "release" } else { "debug" }; + let bin_path = format!("target/{}/echo", profile); + let bpf_path = format!("target/{}/{}/echo", opts.bpf_target, profile); + + // arguments to pass to the application + let mut run_args: Vec<_> = opts.run_args.iter().map(String::as_str).collect(); + if opts.supply_path { + run_args.push("--path"); + run_args.push(bpf_path.as_str()); + }; + + // configure args + let mut args: Vec<_> = opts.runner.trim().split_terminator(' ').collect(); + args.push(bin_path.as_str()); + args.append(&mut run_args); + + // spawn the command + let err = Command::new(args.get(0).expect("No first argument")) + .args(args.iter().skip(1)) + .exec(); + + // we shouldn't get here unless the command failed to spawn + Err(anyhow::Error::from(err).context(format!("Failed to run `{}`", args.join(" ")))) +}