commit f3ee525588b10359100859c1fdb3525f83798958 Author: TuDatTr Date: Tue Feb 20 11:13:23 2024 +0100 Initial commit Signed-off-by: TuDatTr diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..35049cb --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +xtask = "run --package xtask --" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..54f741e --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +### https://raw.github.com/github/gitignore/master/Rust.gitignore + +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk diff --git a/.vim/coc-settings.json b/.vim/coc-settings.json new file mode 100644 index 0000000..a92e5c8 --- /dev/null +++ b/.vim/coc-settings.json @@ -0,0 +1,3 @@ +{ + "rust-analyzer.linkedProjects": ["Cargo.toml", "aya-playground-ebpf/Cargo.toml"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a92e5c8 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "rust-analyzer.linkedProjects": ["Cargo.toml", "aya-playground-ebpf/Cargo.toml"] +} diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9f3628e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["xtask", "aya-playground", "aya-playground-common"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..2e112a3 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# aya-playground + +## Prerequisites + +1. Install bpf-linker: `cargo install bpf-linker` + +## Build eBPF + +```bash +cargo xtask build-ebpf +``` + +To perform a release build you can use the `--release` flag. +You may also change the target architecture with the `--target` flag. + +## Build Userspace + +```bash +cargo build +``` + +## Run + +```bash +RUST_LOG=info cargo xtask run +``` +# Full Execution +```bash +cargo xtask build-ebpf +RUST_LOG=info cargo xtask run -- --iface wlp2s0 +``` diff --git a/README.md~ b/README.md~ new file mode 100644 index 0000000..14926d9 --- /dev/null +++ b/README.md~ @@ -0,0 +1,28 @@ +# aya-playground + +## Prerequisites + +1. Install bpf-linker: `cargo install bpf-linker` + +## Build eBPF + +```bash +cargo xtask build-ebpf +``` + +To perform a release build you can use the `--release` flag. +You may also change the target architecture with the `--target` flag. + +## Build Userspace + +```bash +cargo build +``` + +## Run + +```bash +RUST_LOG=info cargo xtask run +``` +cargo xtask build-ebpf +RUST_LOG=info cargo xtask run -- --iface wlp2s0 diff --git a/aya-playground-common/Cargo.toml b/aya-playground-common/Cargo.toml new file mode 100644 index 0000000..11827be --- /dev/null +++ b/aya-playground-common/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "aya-playground-common" +version = "0.1.0" +edition = "2021" + +[features] +default = [] +user = ["aya"] + +[dependencies] +aya = { git = "https://github.com/aya-rs/aya", optional = true } + +[lib] +path = "src/lib.rs" diff --git a/aya-playground-common/src/lib.rs b/aya-playground-common/src/lib.rs new file mode 100644 index 0000000..0c9ac1a --- /dev/null +++ b/aya-playground-common/src/lib.rs @@ -0,0 +1 @@ +#![no_std] diff --git a/aya-playground-ebpf/.cargo/config.toml b/aya-playground-ebpf/.cargo/config.toml new file mode 100644 index 0000000..4302a7f --- /dev/null +++ b/aya-playground-ebpf/.cargo/config.toml @@ -0,0 +1,6 @@ +[build] +target-dir = "../target" +target = "bpfel-unknown-none" + +[unstable] +build-std = ["core"] diff --git a/aya-playground-ebpf/.vim/coc-settings.json b/aya-playground-ebpf/.vim/coc-settings.json new file mode 100644 index 0000000..e2211a6 --- /dev/null +++ b/aya-playground-ebpf/.vim/coc-settings.json @@ -0,0 +1,4 @@ +{ + "rust-analyzer.cargo.target": "bpfel-unknown-none", + "rust-analyzer.checkOnSave.allTargets": false +} diff --git a/aya-playground-ebpf/.vscode/settings.json b/aya-playground-ebpf/.vscode/settings.json new file mode 100644 index 0000000..e2211a6 --- /dev/null +++ b/aya-playground-ebpf/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "rust-analyzer.cargo.target": "bpfel-unknown-none", + "rust-analyzer.checkOnSave.allTargets": false +} diff --git a/aya-playground-ebpf/Cargo.toml b/aya-playground-ebpf/Cargo.toml new file mode 100644 index 0000000..5239bf5 --- /dev/null +++ b/aya-playground-ebpf/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "aya-playground-ebpf" +version = "0.1.0" +edition = "2021" + +[dependencies] +aya-bpf = { git = "https://github.com/aya-rs/aya" } +aya-log-ebpf = { git = "https://github.com/aya-rs/aya" } +aya-playground-common = { path = "../aya-playground-common" } +network-types = "0.0.4" + +[[bin]] +name = "aya-playground" +path = "src/main.rs" + +[profile.dev] +opt-level = 3 +debug = false +debug-assertions = false +overflow-checks = false +lto = true +panic = "abort" +incremental = false +codegen-units = 1 +rpath = false + +[profile.release] +lto = true +panic = "abort" +codegen-units = 1 + +[workspace] +members = [] diff --git a/aya-playground-ebpf/Cargo.toml~ b/aya-playground-ebpf/Cargo.toml~ new file mode 100644 index 0000000..8d95856 --- /dev/null +++ b/aya-playground-ebpf/Cargo.toml~ @@ -0,0 +1,32 @@ +[package] +name = "aya-playground-ebpf" +version = "0.1.0" +edition = "2021" + +[dependencies] +aya-bpf = { git = "https://github.com/aya-rs/aya" } +aya-log-ebpf = { git = "https://github.com/aya-rs/aya" } +aya-playground-common = { path = "../aya-playground-common" } + +[[bin]] +name = "aya-playground" +path = "src/main.rs" + +[profile.dev] +opt-level = 3 +debug = false +debug-assertions = false +overflow-checks = false +lto = true +panic = "abort" +incremental = false +codegen-units = 1 +rpath = false + +[profile.release] +lto = true +panic = "abort" +codegen-units = 1 + +[workspace] +members = [] diff --git a/aya-playground-ebpf/rust-toolchain.toml b/aya-playground-ebpf/rust-toolchain.toml new file mode 100644 index 0000000..24ce391 --- /dev/null +++ b/aya-playground-ebpf/rust-toolchain.toml @@ -0,0 +1,13 @@ +[toolchain] +channel = "nightly" +# The source code of rustc, provided by the rust-src component, is needed for +# building eBPF programs. +components = [ + "cargo", + "clippy", + "rust-docs", + "rust-src", + "rust-std", + "rustc", + "rustfmt", +] diff --git a/aya-playground-ebpf/src/main.rs b/aya-playground-ebpf/src/main.rs new file mode 100644 index 0000000..73f02d3 --- /dev/null +++ b/aya-playground-ebpf/src/main.rs @@ -0,0 +1,67 @@ +#![no_std] +#![no_main] + +use aya_bpf::{bindings::xdp_action, macros::xdp, programs::XdpContext}; +use aya_log_ebpf::info; + +use core::mem; +use network_types::{ + eth::{EthHdr, EtherType}, + ip::{IpProto, Ipv4Hdr}, + tcp::TcpHdr, + udp::UdpHdr, +}; + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + unsafe { core::hint::unreachable_unchecked() } +} + +#[xdp] +pub fn aya_playground(ctx: XdpContext) -> u32 { + match try_xdp_firewall(ctx) { + Ok(ret) => ret, + Err(_) => xdp_action::XDP_ABORTED, + } +} + +#[inline(always)] +fn ptr_at(ctx: &XdpContext, offset: usize) -> Result<*const T, ()> { + let start = ctx.data(); + let end = ctx.data_end(); + let len = mem::size_of::(); + + if start + offset + len > end { + Err(()) + } else { + Ok((start + offset) as *const T) + } +} + +fn try_xdp_firewall(ctx: XdpContext) -> Result { + let ethhdr: *const EthHdr = ptr_at(&ctx, 0)?; + // Only pass Ipv4 along pipeline, ALLOW rest + match unsafe { (*ethhdr).ether_type } { + EtherType::Ipv4 => {} + _ => return Ok(xdp_action::XDP_PASS), + } + + let ipv4hdr: *const Ipv4Hdr = ptr_at(&ctx, EthHdr::LEN)?; + let source_addr = u32::from_be(unsafe { (*ipv4hdr).src_addr }); + + let source_port = match unsafe { (*ipv4hdr).proto } { + IpProto::Tcp => { + let tcphdr: *const TcpHdr = ptr_at(&ctx, Ipv4Hdr::LEN)?; + u16::from_be(unsafe { (*tcphdr).source }) + } + IpProto::Udp => { + let udphdr: *const UdpHdr = ptr_at(&ctx, Ipv4Hdr::LEN)?; + u16::from_be(unsafe { (*udphdr).source }) + } + _ => return Err(()), + }; + + info!(&ctx, "SRC IP: {:i}, SRC PORT: {}", source_addr, source_port); + + Ok(xdp_action::XDP_PASS) +} diff --git a/aya-playground/Cargo.toml b/aya-playground/Cargo.toml new file mode 100644 index 0000000..24bb035 --- /dev/null +++ b/aya-playground/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "aya-playground" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +aya = { git = "https://github.com/aya-rs/aya", features = ["async_tokio"] } +aya-log = { git = "https://github.com/aya-rs/aya" } +clap = { version = "4.1", features = ["derive"] } +aya-playground-common = { path = "../aya-playground-common", features = ["user"] } +anyhow = "1" +env_logger = "0.10" +libc = "0.2" +log = "0.4" +tokio = { version = "1.25", features = ["macros", "rt", "rt-multi-thread", "net", "signal"] } + +[[bin]] +name = "aya-playground" +path = "src/main.rs" diff --git a/aya-playground/src/main.rs b/aya-playground/src/main.rs new file mode 100644 index 0000000..06414cc --- /dev/null +++ b/aya-playground/src/main.rs @@ -0,0 +1,58 @@ +use anyhow::Context; +use aya::programs::{Xdp, XdpFlags}; +use aya::{include_bytes_aligned, Bpf}; +use aya_log::BpfLogger; +use clap::Parser; +use log::{info, warn, debug}; +use tokio::signal; + +#[derive(Debug, Parser)] +struct Opt { + #[clap(short, long, default_value = "eth0")] + iface: String, +} + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + let opt = Opt::parse(); + + env_logger::init(); + + // Bump the memlock rlimit. This is needed for older kernels that don't use the + // new memcg based accounting, see https://lwn.net/Articles/837122/ + let rlim = libc::rlimit { + rlim_cur: libc::RLIM_INFINITY, + rlim_max: libc::RLIM_INFINITY, + }; + let ret = unsafe { libc::setrlimit(libc::RLIMIT_MEMLOCK, &rlim) }; + if ret != 0 { + debug!("remove limit on locked memory failed, ret is: {}", ret); + } + + // 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)] + let mut bpf = Bpf::load(include_bytes_aligned!( + "../../target/bpfel-unknown-none/debug/aya-playground" + ))?; + #[cfg(not(debug_assertions))] + let mut bpf = Bpf::load(include_bytes_aligned!( + "../../target/bpfel-unknown-none/release/aya-playground" + ))?; + if let Err(e) = BpfLogger::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 Xdp = bpf.program_mut("aya_playground").unwrap().try_into()?; + program.load()?; + program.attach(&opt.iface, XdpFlags::default()) + .context("failed to attach the XDP program with default flags - try changing XdpFlags::default() to XdpFlags::SKB_MODE")?; + + info!("Waiting for Ctrl-C..."); + signal::ctrl_c().await?; + info!("Exiting..."); + + Ok(()) +} diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 index 0000000..c4dea5d --- /dev/null +++ b/xtask/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "xtask" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1" +clap = { version = "4.1", features = ["derive"] } diff --git a/xtask/src/build_ebpf.rs b/xtask/src/build_ebpf.rs new file mode 100644 index 0000000..a5cf098 --- /dev/null +++ b/xtask/src/build_ebpf.rs @@ -0,0 +1,67 @@ +use std::{path::PathBuf, process::Command}; + +use clap::Parser; + +#[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(Debug, Parser)] +pub struct Options { + /// Set the endianness of the BPF target + #[clap(default_value = "bpfel-unknown-none", long)] + pub target: Architecture, + /// Build the release target + #[clap(long)] + pub release: bool, +} + +pub fn build_ebpf(opts: Options) -> Result<(), anyhow::Error> { + let dir = PathBuf::from("aya-playground-ebpf"); + let target = format!("--target={}", opts.target); + let mut args = vec![ + "build", + target.as_str(), + "-Z", + "build-std=core", + ]; + if opts.release { + args.push("--release") + } + + // Command::new creates a child process which inherits all env variables. This means env + // vars set by the cargo xtask command are also inherited. RUSTUP_TOOLCHAIN is removed + // so the rust-toolchain.toml file in the -ebpf folder is honored. + + let status = Command::new("cargo") + .current_dir(dir) + .env_remove("RUSTUP_TOOLCHAIN") + .args(&args) + .status() + .expect("failed to build bpf program"); + assert!(status.success()); + Ok(()) +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 0000000..c1c594e --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,33 @@ +mod build_ebpf; +mod run; + +use std::process::exit; + +use clap::Parser; + +#[derive(Debug, Parser)] +pub struct Options { + #[clap(subcommand)] + command: Command, +} + +#[derive(Debug, Parser)] +enum Command { + BuildEbpf(build_ebpf::Options), + Run(run::Options), +} + +fn main() { + let opts = Options::parse(); + + 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/xtask/src/run.rs b/xtask/src/run.rs new file mode 100644 index 0000000..880544b --- /dev/null +++ b/xtask/src/run.rs @@ -0,0 +1,70 @@ +use std::process::Command; + +use anyhow::Context as _; +use clap::Parser; + +use crate::build_ebpf::{build_ebpf, Architecture, Options as BuildOptions}; + +#[derive(Debug, Parser)] +pub struct Options { + /// Set the endianness of the BPF target + #[clap(default_value = "bpfel-unknown-none", long)] + pub bpf_target: Architecture, + /// Build and run the release target + #[clap(long)] + pub release: bool, + /// The command used to wrap your application + #[clap(short, long, default_value = "sudo -E")] + pub runner: String, + /// Arguments to pass to your application + #[clap(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/{profile}/aya-playground"); + + // arguments to pass to the application + let mut run_args: Vec<_> = opts.run_args.iter().map(String::as_str).collect(); + + // configure args + let mut args: Vec<_> = opts.runner.trim().split_terminator(' ').collect(); + args.push(bin_path.as_str()); + args.append(&mut run_args); + + // run the command + let status = Command::new(args.first().expect("No first argument")) + .args(args.iter().skip(1)) + .status() + .expect("failed to run the command"); + + if !status.success() { + anyhow::bail!("Failed to run `{}`", args.join(" ")); + } + Ok(()) +}