From 06b6e29182621ca2c23ddde36a1c8d50cd0059dd Mon Sep 17 00:00:00 2001 From: Brandon Kauffman Date: Fri, 23 Feb 2024 06:31:30 -0500 Subject: [PATCH] init --- .cargo/config.toml | 2 + .gitignore | 13 ++++ .vim/coc-settings.json | 3 + .vscode/settings.json | 3 + Cargo.toml | 2 + README.md | 26 ++++++++ kill-the-devil-common/Cargo.toml | 14 ++++ kill-the-devil-common/src/lib.rs | 1 + kill-the-devil-ebpf/.cargo/config.toml | 6 ++ kill-the-devil-ebpf/.vim/coc-settings.json | 4 ++ kill-the-devil-ebpf/.vscode/settings.json | 4 ++ kill-the-devil-ebpf/Cargo.toml | 32 +++++++++ kill-the-devil-ebpf/rust-toolchain.toml | 13 ++++ kill-the-devil-ebpf/src/main.rs | 75 ++++++++++++++++++++++ kill-the-devil/Cargo.toml | 28 ++++++++ kill-the-devil/src/main.rs | 69 ++++++++++++++++++++ xtask/Cargo.toml | 8 +++ xtask/src/build_ebpf.rs | 67 +++++++++++++++++++ xtask/src/main.rs | 33 ++++++++++ xtask/src/run.rs | 70 ++++++++++++++++++++ 20 files changed, 473 insertions(+) create mode 100644 .cargo/config.toml create mode 100644 .gitignore create mode 100644 .vim/coc-settings.json create mode 100644 .vscode/settings.json create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 kill-the-devil-common/Cargo.toml create mode 100644 kill-the-devil-common/src/lib.rs create mode 100644 kill-the-devil-ebpf/.cargo/config.toml create mode 100644 kill-the-devil-ebpf/.vim/coc-settings.json create mode 100644 kill-the-devil-ebpf/.vscode/settings.json create mode 100644 kill-the-devil-ebpf/Cargo.toml create mode 100644 kill-the-devil-ebpf/rust-toolchain.toml create mode 100644 kill-the-devil-ebpf/src/main.rs create mode 100644 kill-the-devil/Cargo.toml create mode 100644 kill-the-devil/src/main.rs create mode 100644 xtask/Cargo.toml create mode 100644 xtask/src/build_ebpf.rs create mode 100644 xtask/src/main.rs create mode 100644 xtask/src/run.rs 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..4743154 --- /dev/null +++ b/.vim/coc-settings.json @@ -0,0 +1,3 @@ +{ + "rust-analyzer.linkedProjects": ["Cargo.toml", "kill-the-devil-ebpf/Cargo.toml"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4743154 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "rust-analyzer.linkedProjects": ["Cargo.toml", "kill-the-devil-ebpf/Cargo.toml"] +} diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c9f515a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["xtask", "kill-the-devil", "kill-the-devil-common"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..eaa2ae9 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# kill-the-devil + +## 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 +``` diff --git a/kill-the-devil-common/Cargo.toml b/kill-the-devil-common/Cargo.toml new file mode 100644 index 0000000..9bda6d9 --- /dev/null +++ b/kill-the-devil-common/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "kill-the-devil-common" +version = "0.1.0" +edition = "2021" + +[features] +default = [] +user = ["aya"] + +[dependencies] +aya = { git = "https://github.com/aya-rs/aya.git", optional = true } + +[lib] +path = "src/lib.rs" diff --git a/kill-the-devil-common/src/lib.rs b/kill-the-devil-common/src/lib.rs new file mode 100644 index 0000000..0c9ac1a --- /dev/null +++ b/kill-the-devil-common/src/lib.rs @@ -0,0 +1 @@ +#![no_std] diff --git a/kill-the-devil-ebpf/.cargo/config.toml b/kill-the-devil-ebpf/.cargo/config.toml new file mode 100644 index 0000000..4302a7f --- /dev/null +++ b/kill-the-devil-ebpf/.cargo/config.toml @@ -0,0 +1,6 @@ +[build] +target-dir = "../target" +target = "bpfel-unknown-none" + +[unstable] +build-std = ["core"] diff --git a/kill-the-devil-ebpf/.vim/coc-settings.json b/kill-the-devil-ebpf/.vim/coc-settings.json new file mode 100644 index 0000000..e2211a6 --- /dev/null +++ b/kill-the-devil-ebpf/.vim/coc-settings.json @@ -0,0 +1,4 @@ +{ + "rust-analyzer.cargo.target": "bpfel-unknown-none", + "rust-analyzer.checkOnSave.allTargets": false +} diff --git a/kill-the-devil-ebpf/.vscode/settings.json b/kill-the-devil-ebpf/.vscode/settings.json new file mode 100644 index 0000000..e2211a6 --- /dev/null +++ b/kill-the-devil-ebpf/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "rust-analyzer.cargo.target": "bpfel-unknown-none", + "rust-analyzer.checkOnSave.allTargets": false +} diff --git a/kill-the-devil-ebpf/Cargo.toml b/kill-the-devil-ebpf/Cargo.toml new file mode 100644 index 0000000..d65d30e --- /dev/null +++ b/kill-the-devil-ebpf/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "kill-the-devil-ebpf" +version = "0.1.0" +edition = "2021" + +[dependencies] +aya-bpf = { git = "https://github.com/aya-rs/aya.git" } +aya-log-ebpf = { git = "https://github.com/aya-rs/aya.git" } +kill-the-devil-common = { path = "../kill-the-devil-common" } +network-types = "0.0.4" +[[bin]] +name = "kill-the-devil" +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/kill-the-devil-ebpf/rust-toolchain.toml b/kill-the-devil-ebpf/rust-toolchain.toml new file mode 100644 index 0000000..24ce391 --- /dev/null +++ b/kill-the-devil-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/kill-the-devil-ebpf/src/main.rs b/kill-the-devil-ebpf/src/main.rs new file mode 100644 index 0000000..4372e8c --- /dev/null +++ b/kill-the-devil-ebpf/src/main.rs @@ -0,0 +1,75 @@ +#![no_std] +#![no_main] +#![allow(nonstandard_style, dead_code)] + +use aya_bpf::{ + bindings::xdp_action, + macros::{map, xdp}, + maps::HashMap, + programs::XdpContext, +}; +use aya_log_ebpf::info; + +use core::mem; +use network_types::{ + eth::{EthHdr, EtherType}, + ip::Ipv4Hdr, +}; + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + unsafe { core::hint::unreachable_unchecked() } +} + +#[map] // +static BLOCKLIST: HashMap = HashMap::::with_max_entries(1024, 0); + +#[xdp] +pub fn kill_the_devil(ctx: XdpContext) -> u32 { + match try_kill_the_devil(ctx) { + Ok(ret) => ret, + Err(_) => xdp_action::XDP_ABORTED, + } +} + +#[inline(always)] +unsafe 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 { + return Err(()); + } + + let ptr = (start + offset) as *const T; + Ok(&*ptr) +} + +// +fn block_ip(address: u32) -> bool { + unsafe { BLOCKLIST.get(&address).is_some() } +} + +fn try_kill_the_devil(ctx: XdpContext) -> Result { + let ethhdr: *const EthHdr = unsafe { ptr_at(&ctx, 0)? }; + match unsafe { (*ethhdr).ether_type } { + EtherType::Ipv4 => {} + _ => return Ok(xdp_action::XDP_PASS), + } + + let ipv4hdr: *const Ipv4Hdr = unsafe { ptr_at(&ctx, EthHdr::LEN)? }; + let source = u32::from_be(unsafe { (*ipv4hdr).src_addr }); + + // + let action = if block_ip(source) { + xdp_action::XDP_DROP + } else { + xdp_action::XDP_PASS + }; + if action == xdp_action::XDP_DROP { + info!(&ctx, "SRC: {:i}, ACTION: {} DROPPED", source, action); + } + + Ok(action) +} diff --git a/kill-the-devil/Cargo.toml b/kill-the-devil/Cargo.toml new file mode 100644 index 0000000..576def7 --- /dev/null +++ b/kill-the-devil/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "kill-the-devil" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +aya = { git = "https://github.com/aya-rs/aya.git", features = ["async_tokio"] } +aya-log = { git = "https://github.com/aya-rs/aya.git" } +clap = { version = "4.1", features = ["derive"] } +kill-the-devil-common = { path = "../kill-the-devil-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 = "kill-the-devil" +path = "src/main.rs" diff --git a/kill-the-devil/src/main.rs b/kill-the-devil/src/main.rs new file mode 100644 index 0000000..266b31a --- /dev/null +++ b/kill-the-devil/src/main.rs @@ -0,0 +1,69 @@ +use anyhow::Context; +use aya::{ + include_bytes_aligned, + maps::HashMap, + programs::{Xdp, XdpFlags}, + Bpf, +}; +use aya_log::BpfLogger; +use clap::Parser; +use log::{info, warn}; +use std::net::Ipv4Addr; +use tokio::signal; + +#[derive(Debug, Parser)] +struct Opt { + #[clap(short, long, default_value = "eth0")] + iface: String, + #[clap(short, long, default_value = "1.1.1.1")] + blocklist: String, +} + +#[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 `Bpf::load_file` instead. + #[cfg(debug_assertions)] + let mut bpf = Bpf::load(include_bytes_aligned!( + "../../target/bpfel-unknown-none/debug/kill-the-devil" + ))?; + #[cfg(not(debug_assertions))] + let mut bpf = Bpf::load(include_bytes_aligned!( + "../../target/bpfel-unknown-none/release/kill-the-devil" + ))?; + 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("kill_the_devil").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")?; + + // + let mut blocklist: HashMap<_, u32, u32> = HashMap::try_from(bpf.map_mut("BLOCKLIST").unwrap())?; + + // + + let addrs = &opt + .blocklist + .split_terminator('.') + .map(|s| s.parse::().unwrap()) + .collect::>(); + let block_addr: u32 = Ipv4Addr::new(addrs[0], addrs[1], addrs[2], addrs[3]).try_into()?; + + // + blocklist.insert(block_addr, 0, 0)?; + + 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..50fd6a0 --- /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("kill-the-devil-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..efaa6e0 --- /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}/kill-the-devil"); + + // 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(()) +}