diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4d40e9..3e17667 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,9 @@ jobs: strategy: fail-fast: false matrix: + rust: + - stable + - 1.80.1 program: - kprobe - kretprobe @@ -47,10 +50,11 @@ jobs: - uses: dtolnay/rust-toolchain@nightly with: - components: rust-src + components: rust-src,rustfmt - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@master with: + toolchain: ${{ matrix.rust }} components: clippy - uses: Swatinem/rust-cache@v2 diff --git a/Cargo.toml b/Cargo.toml index b411146..a833d12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ aya-log = { version = "0.2.1", default-features = false } aya-log-ebpf = { version = "0.1.1", default-features = false } anyhow = { version = "1", default-features = false } +cargo_metadata = { version = "0.18.0", default-features = false } # `std` feature is currently required to build `clap`. # # See https://github.com/clap-rs/clap/blob/61f5ee5/clap_builder/src/lib.rs#L15. @@ -18,18 +19,14 @@ env_logger = { version = "0.11.5", default-features = false } libc = { version = "0.2.159", default-features = false } log = { version = "0.4.22", default-features = false } tokio = { version = "1.40.0", default-features = false } +which = { version = "6.0.0", default-features = false } [profile.dev] -opt-level = 3 -debug = false -overflow-checks = false -lto = true panic = "abort" -incremental = false -codegen-units = 1 -rpath = false [profile.release] -lto = true panic = "abort" + +[profile.release.package.{{project-name}}-ebpf] +debug = 2 codegen-units = 1 diff --git a/README.md b/README.md index bee17f6..c870678 100644 --- a/README.md +++ b/README.md @@ -4,29 +4,10 @@ 1. Install bpf-linker: `cargo install bpf-linker` -## Build eBPF +## Build & Run -```bash -cargo xtask build-ebpf -``` +Use `cargo build`, `cargo check`, etc. as normal. Run your program with `xtask run`. -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 -``` - -## Build eBPF and Userspace - -```bash -cargo xtask build -``` - -## Run - -```bash -RUST_LOG=info cargo xtask run -``` +Cargo build scripts are used to automatically build the eBPF correctly and include it in the +program. When not using `xtask run`, eBPF code generation is skipped for a faster developer +experience; this compromise necessitates the use of `xtask` to actually build the eBPF. diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..53f7b6d --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,4 @@ +group_imports = "StdExternalCrate" +imports_granularity = "Crate" +reorder_imports = true +unstable_features = true diff --git a/test.sh b/test.sh index 87a7e6a..43984bd 100755 --- a/test.sh +++ b/test.sh @@ -3,60 +3,61 @@ set -ex TEMPLATE_DIR=$1 -if [ -z "$TEMPLATE_DIR" ]; then echo "template dir required"; exit 1; fi +if [ -z "${TEMPLATE_DIR}" ]; then echo "template dir required"; exit 1; fi PROG_TYPE=$2 -if [ -z "$PROG_TYPE" ]; then echo "program type required"; exit 1; fi +if [ -z "${PROG_TYPE}" ]; then echo "program type required"; exit 1; fi TMP_DIR=$(mktemp -d) clean_up() { + # shellcheck disable=SC2317 rm -rf "${TMP_DIR}" } trap clean_up EXIT -pushd $TMP_DIR -case "$PROG_TYPE" in +pushd "${TMP_DIR}" +case "${PROG_TYPE}" in "cgroup_sockopt") - ADDITIONAL_ARGS="-d sockopt_target=getsockopt" + ADDITIONAL_ARGS=(-d sockopt_target=getsockopt) ;; "classifier"|"cgroup_skb") - ADDITIONAL_ARGS="-d direction=Ingress" + ADDITIONAL_ARGS=(-d direction=Ingress) ;; "fentry"|"fexit") - ADDITIONAL_ARGS="-d fn_name=try_to_wake_up" + ADDITIONAL_ARGS=(-d fn_name=try_to_wake_up) ;; "kprobe"|"kretprobe") - ADDITIONAL_ARGS="-d kprobe=test" + ADDITIONAL_ARGS=(-d kprobe=test) ;; "lsm") - ADDITIONAL_ARGS="-d lsm_hook=file_open" + ADDITIONAL_ARGS=(-d lsm_hook=file_open) ;; "raw_tracepoint") - ADDITIONAL_ARGS="-d tracepoint_name=sys_enter" + ADDITIONAL_ARGS=(-d tracepoint_name=sys_enter) ;; "sk_msg") - ADDITIONAL_ARGS="-d sock_map=SOCK_MAP" + ADDITIONAL_ARGS=(-d sock_map=SOCK_MAP) ;; "tp_btf") - ADDITIONAL_ARGS="-d tracepoint_name=net_dev_queue" + ADDITIONAL_ARGS=(-d tracepoint_name=net_dev_queue) ;; "tracepoint") - ADDITIONAL_ARGS="-d tracepoint_category=net -d tracepoint_name=net_dev_queue" + ADDITIONAL_ARGS=(-d tracepoint_category=net -d tracepoint_name=net_dev_queue) ;; "uprobe"|"uretprobe") - ADDITIONAL_ARGS="-d uprobe_target=testlib -d uprobe_fn_name=testfn" + ADDITIONAL_ARGS=(-d uprobe_target=testlib -d uprobe_fn_name=testfn) ;; *) - ADDITIONAL_ARGS='' + ADDITIONAL_ARGS=() esac -cargo generate --path "${TEMPLATE_DIR}" -n test -d program_type="${PROG_TYPE}" ${ADDITIONAL_ARGS} +cargo generate --path "${TEMPLATE_DIR}" -n test -d program_type="${PROG_TYPE}" "${ADDITIONAL_ARGS[@]}" pushd test -cargo xtask build -cargo xtask build --release +cargo +nightly fmt --all -- --check +cargo build --package test +cargo build --package test --release # We cannot run clippy over the whole workspace at once due to feature unification. Since both test -# and test-ebpf both depend on test-common and test activates test-common's aya dependency, we end -# up trying to compile the panic handler twice: once from the bpf program, and again from std via -# aya. +# and test-ebpf depend on test-common and test activates test-common's aya dependency, we end up +# trying to compile the panic handler twice: once from the bpf program, and again from std via aya. cargo clippy --exclude test-ebpf --all-targets --workspace -- --deny warnings cargo clippy --package test-ebpf --all-targets -- --deny warnings popd diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index ed6bada..6f639a8 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -4,5 +4,5 @@ version = "0.1.0" edition = "2021" [dependencies] -anyhow = { workspace = true } +anyhow = { workspace = true, default-features = true } clap = { workspace = true, default-features = true, features = ["derive"] } diff --git a/xtask/src/build.rs b/xtask/src/build.rs deleted file mode 100644 index 04709d9..0000000 --- a/xtask/src/build.rs +++ /dev/null @@ -1,41 +0,0 @@ -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, -} - -/// Build our ebpf program and the userspace program. -pub fn build(opts: Options) -> Result<(), anyhow::Error> { - let Options { - bpf_target, - release, - } = opts; - - // Build our ebpf program. - build_ebpf(BuildOptions { - target: bpf_target, - release, - })?; - - // Build our userspace program. - let mut cmd = Command::new("cargo"); - cmd.arg("build"); - if release { - cmd.arg("--release"); - } - let status = cmd.status().context("failed to build userspace")?; - anyhow::ensure!(status.success(), "failed to build userspace program: {}", status); - - Ok(()) -} diff --git a/xtask/src/build_ebpf.rs b/xtask/src/build_ebpf.rs deleted file mode 100644 index ca83f1d..0000000 --- a/xtask/src/build_ebpf.rs +++ /dev/null @@ -1,68 +0,0 @@ -use std::process::Command; - -use anyhow::Context as _; -use clap::Parser; - -#[derive(Debug, Clone)] -pub enum Architecture { - BpfEl, - BpfEb, -} - -impl Architecture { - pub fn as_str(&self) -> &'static str { - match self { - Architecture::BpfEl => "bpfel-unknown-none", - Architecture::BpfEb => "bpfeb-unknown-none", - } - } -} - -impl std::str::FromStr for Architecture { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - Ok(match s { - "bpfel-unknown-none" => Architecture::BpfEl, - "bpfeb-unknown-none" => Architecture::BpfEb, - _ => return Err("invalid target"), - }) - } -} - -impl std::fmt::Display for Architecture { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(self.as_str()) - } -} - -#[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 Options { target, release } = opts; - - let mut cmd = Command::new("cargo"); - cmd.current_dir("{{project-name}}-ebpf") - // 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. - .env_remove("RUSTUP_TOOLCHAIN") - .args(["build", "--target", target.as_str()]); - - if release { - cmd.arg("--release"); - } - - let status = cmd.status().context("failed to build bpf program")?; - anyhow::ensure!(status.success(), "failed to build bpf program: {}", status); - - Ok(()) -} diff --git a/xtask/src/lib.rs b/xtask/src/lib.rs new file mode 100644 index 0000000..fd8cbb0 --- /dev/null +++ b/xtask/src/lib.rs @@ -0,0 +1 @@ +pub const AYA_BUILD_EBPF: &str = "AYA_BUILD_EBPF"; diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 5079458..b59e543 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,9 +1,6 @@ -mod build_ebpf; -mod build; mod run; -use std::process::exit; - +use anyhow::Result; use clap::Parser; #[derive(Debug, Parser)] @@ -14,23 +11,13 @@ pub struct Options { #[derive(Debug, Parser)] enum Command { - BuildEbpf(build_ebpf::Options), - Build(build::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), - Build(opts) => build::build(opts), - }; +fn main() -> Result<()> { + let Options { command } = Parser::parse(); - if let Err(e) = ret { - eprintln!("{e:#}"); - exit(1); + match command { + Command::Run(opts) => run::run(opts), } } diff --git a/xtask/src/run.rs b/xtask/src/run.rs index d50bd11..16c80a8 100644 --- a/xtask/src/run.rs +++ b/xtask/src/run.rs @@ -1,55 +1,47 @@ -use std::process::Command; +use std::{ffi::OsString, process::Command}; -use anyhow::Context as _; +use anyhow::{bail, Context as _, Result}; use clap::Parser; - -use crate::{build::{build, Options as BuildOptions}, build_ebpf::Architecture}; +use xtask::AYA_BUILD_EBPF; #[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 + /// Build and run the release target. #[clap(long)] - pub release: bool, - /// The command used to wrap your application + 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, + runner: String, + /// Arguments to pass to your application. + #[clap(global = true, last = true)] + run_args: Vec, } - -/// Build and run the project -pub fn run(opts: Options) -> Result<(), anyhow::Error> { - // Build our ebpf program and the project - build(BuildOptions{ - bpf_target: opts.bpf_target, - release: opts.release, - }).context("Error while building project")?; - - // profile we are building (release or debug) - let profile = if opts.release { "release" } else { "debug" }; - let bin_path = format!("target/{profile}/{{project-name}}"); - - // 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)) +/// Build and run the project. +pub fn run(opts: Options) -> Result<()> { + let Options { + release, + runner, + run_args, + } = opts; + + let mut cmd = Command::new("cargo"); + cmd.env(AYA_BUILD_EBPF, "true"); + cmd.args(["run", "--package", "{{project-name}}", "--config"]); + if release { + cmd.arg(format!("target.\"cfg(all())\".runner=\"{}\"", runner)); + cmd.arg("--release"); + } else { + cmd.arg(format!("target.\"cfg(all())\".runner=\"{}\"", runner)); + } + if !run_args.is_empty() { + cmd.arg("--").args(run_args); + } + let status = cmd .status() - .expect("failed to run the command"); - - if !status.success() { - anyhow::bail!("Failed to run `{}`", args.join(" ")); + .with_context(|| format!("failed to run {cmd:?}"))?; + if status.code() != Some(0) { + bail!("{cmd:?} failed: {status:?}") } Ok(()) } diff --git a/{{project-name}}-ebpf/Cargo.toml b/{{project-name}}-ebpf/Cargo.toml index 4f5f274..578c50e 100644 --- a/{{project-name}}-ebpf/Cargo.toml +++ b/{{project-name}}-ebpf/Cargo.toml @@ -9,6 +9,10 @@ edition = "2021" aya-ebpf = { workspace = true } aya-log-ebpf = { workspace = true } +[build-dependencies] +which = { workspace = true } +xtask = { path = "../xtask" } + [[bin]] name = "{{ project-name }}" path = "src/main.rs" diff --git a/{{project-name}}-ebpf/build.rs b/{{project-name}}-ebpf/build.rs new file mode 100644 index 0000000..101ade2 --- /dev/null +++ b/{{project-name}}-ebpf/build.rs @@ -0,0 +1,30 @@ +use std::env; + +use which::which; +use xtask::AYA_BUILD_EBPF; + +/// Building this crate has an undeclared dependency on the `bpf-linker` binary. This would be +/// better expressed by [artifact-dependencies][bindeps] but issues such as +/// https://github.com/rust-lang/cargo/issues/12385 make their use impractical for the time being. +/// +/// This file implements an imperfect solution: it causes cargo to rebuild the crate whenever the +/// mtime of `which bpf-linker` changes. Note that possibility that a new bpf-linker is added to +/// $PATH ahead of the one used as the cache key still exists. Solving this in the general case +/// would require rebuild-if-changed-env=PATH *and* rebuild-if-changed={every-directory-in-PATH} +/// which would likely mean far too much cache invalidation. +/// +/// [bindeps]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html?highlight=feature#artifact-dependencies +fn main() { + println!("cargo:rerun-if-env-changed={}", AYA_BUILD_EBPF); + + let build_ebpf = env::var(AYA_BUILD_EBPF) + .as_deref() + .map(str::parse) + .map(Result::unwrap) + .unwrap_or_default(); + + if build_ebpf { + let bpf_linker = which("bpf-linker").unwrap(); + println!("cargo:rerun-if-changed={}", bpf_linker.to_str().unwrap()); + } +} diff --git a/{{project-name}}-ebpf/src/lib.rs b/{{project-name}}-ebpf/src/lib.rs new file mode 100644 index 0000000..3ac3e59 --- /dev/null +++ b/{{project-name}}-ebpf/src/lib.rs @@ -0,0 +1,3 @@ +#![no_std] + +// This file exists to enable the library target. diff --git a/{{project-name}}-ebpf/src/main.rs b/{{project-name}}-ebpf/src/main.rs index 9a0a61f..63be060 100644 --- a/{{project-name}}-ebpf/src/main.rs +++ b/{{project-name}}-ebpf/src/main.rs @@ -34,13 +34,10 @@ fn try_{{crate_name}}(ctx: RetProbeContext) -> Result { Ok(0) } {%- when "fentry" %} -use aya_ebpf::{ - macros::fentry, - programs::FEntryContext, -}; +use aya_ebpf::{macros::fentry, programs::FEntryContext}; use aya_log_ebpf::info; -#[fentry(function="{{fn_name}}")] +#[fentry(function = "{{fn_name}}")] pub fn {{crate_name}}(ctx: FEntryContext) -> u32 { match try_{{crate_name}}(ctx) { Ok(ret) => ret, @@ -53,13 +50,10 @@ fn try_{{crate_name}}(ctx: FEntryContext) -> Result { Ok(0) } {%- when "fexit" %} -use aya_ebpf::{ - macros::fexit, - programs::FExitContext, -}; +use aya_ebpf::{macros::fexit, programs::FExitContext}; use aya_log_ebpf::info; -#[fexit(function="{{fn_name}}")] +#[fexit(function = "{{fn_name}}")] pub fn {{crate_name}}(ctx: FExitContext) -> u32 { match try_{{crate_name}}(ctx) { Ok(ret) => ret, @@ -72,10 +66,7 @@ fn try_{{crate_name}}(ctx: FExitContext) -> Result { Ok(0) } {%- when "uprobe" %} -use aya_ebpf::{ - macros::uprobe, - programs::ProbeContext, -}; +use aya_ebpf::{macros::uprobe, programs::ProbeContext}; use aya_log_ebpf::info; #[uprobe] @@ -91,10 +82,7 @@ fn try_{{crate_name}}(ctx: ProbeContext) -> Result { Ok(0) } {%- when "uretprobe" %} -use aya_ebpf::{ - macros::uretprobe, - programs::RetProbeContext, -}; +use aya_ebpf::{macros::uretprobe, programs::RetProbeContext}; use aya_log_ebpf::info; #[uretprobe] @@ -110,10 +98,7 @@ fn try_{{crate_name}}(ctx: RetProbeContext) -> Result { Ok(0) } {%- when "sock_ops" %} -use aya_ebpf::{ - macros::sock_ops, - programs::SockOpsContext, -}; +use aya_ebpf::{macros::sock_ops, programs::SockOpsContext}; use aya_log_ebpf::info; #[sock_ops] @@ -135,7 +120,6 @@ use aya_ebpf::{ programs::SkMsgContext, }; use aya_log_ebpf::info; - use {{crate_name}}_common::SockKey; #[map] @@ -186,10 +170,7 @@ fn try_{{crate_name}}(ctx: TcContext) -> Result { Ok(TC_ACT_PIPE) } {%- when "cgroup_skb" %} -use aya_ebpf::{ - macros::cgroup_skb, - programs::SkBuffContext, -}; +use aya_ebpf::{macros::cgroup_skb, programs::SkBuffContext}; use aya_log_ebpf::info; #[cgroup_skb] @@ -205,10 +186,7 @@ fn try_{{crate_name}}(ctx: SkBuffContext) -> Result { Ok(0) } {%- when "tracepoint" %} -use aya_ebpf::{ - macros::tracepoint, - programs::TracePointContext, -}; +use aya_ebpf::{macros::tracepoint, programs::TracePointContext}; use aya_log_ebpf::info; #[tracepoint] @@ -224,10 +202,7 @@ fn try_{{crate_name}}(ctx: TracePointContext) -> Result { Ok(0) } {%- when "lsm" %} -use aya_ebpf::{ - macros::lsm, - programs::LsmContext, -}; +use aya_ebpf::{macros::lsm, programs::LsmContext}; use aya_log_ebpf::info; #[lsm(hook = "{{lsm_hook}}")] @@ -243,13 +218,10 @@ fn try_{{lsm_hook}}(ctx: LsmContext) -> Result { Ok(0) } {%- when "tp_btf" %} -use aya_ebpf::{ - macros::btf_tracepoint, - programs::BtfTracePointContext, -}; +use aya_ebpf::{macros::btf_tracepoint, programs::BtfTracePointContext}; use aya_log_ebpf::info; -#[btf_tracepoint(function="{{tracepoint_name}}")] +#[btf_tracepoint(function = "{{tracepoint_name}}")] pub fn {{tracepoint_name}}(ctx: BtfTracePointContext) -> i32 { match try_{{tracepoint_name}}(ctx) { Ok(ret) => ret, @@ -262,20 +234,14 @@ fn try_{{tracepoint_name}}(ctx: BtfTracePointContext) -> Result { Ok(0) } {%- when "socket_filter" %} -use aya_ebpf::{ - macros::socket_filter, - programs::SkBuffContext, -}; +use aya_ebpf::{macros::socket_filter, programs::SkBuffContext}; #[socket_filter] pub fn {{crate_name}}(_ctx: SkBuffContext) -> i64 { 0 } {%- when "cgroup_sysctl" %} -use aya_ebpf::{ - macros::cgroup_sysctl, - programs::SysctlContext, -}; +use aya_ebpf::{macros::cgroup_sysctl, programs::SysctlContext}; use aya_log_ebpf::info; #[cgroup_sysctl] @@ -310,7 +276,7 @@ fn try_{{crate_name}}(ctx: SockoptContext) -> Result { use aya_ebpf::{macros::raw_tracepoint, programs::RawTracePointContext}; use aya_log_ebpf::info; -#[raw_tracepoint(tracepoint="{{tracepoint_name}}")] +#[raw_tracepoint(tracepoint = "{{tracepoint_name}}")] pub fn {{crate_name}}(ctx: RawTracePointContext) -> i32 { match try_{{crate_name}}(ctx) { Ok(ret) => ret, diff --git a/{{project-name}}/Cargo.toml b/{{project-name}}/Cargo.toml index 83c71d2..c5af123 100644 --- a/{{project-name}}/Cargo.toml +++ b/{{project-name}}/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [dependencies] {{project-name}}-common = { path = "../{{project-name}}-common", features = ["user"] } -anyhow = { workspace = true } +anyhow = { workspace = true, default-features = true } aya = { workspace = true } aya-log = { workspace = true } env_logger = {workspace = true } @@ -18,6 +18,23 @@ tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread", "net" clap = { workspace = true, features = ["derive"] } {% endif -%} +[build-dependencies] +cargo_metadata = { workspace = true } +# TODO(https://github.com/rust-lang/cargo/issues/12375): this should be an artifact dependency, but +# it's not possible to tell cargo to use `-Z build-std` to build it. We cargo-in-cargo in the build +# script to build this, but we want to teach cargo about the dependecy so that cache invalidation +# works properly. +# +# Note also that https://github.com/rust-lang/cargo/issues/10593 occurs when `target = ...` is added +# to an artifact dependency; it seems possible to work around that by setting `resolver = "1"` in +# Cargo.toml in the workspace root. +# +# Finally note that *any* usage of `artifact = ...` in *any* Cargo.toml in the workspace breaks +# workflows with stable cargo; stable cargo outright refuses to load manifests that use unstable +# features. +{{project-name}}-ebpf = { path = "../{{project-name}}-ebpf" } +xtask = { path = "../xtask"} + [[bin]] name = "{{project-name}}" path = "src/main.rs" diff --git a/{{project-name}}/build.rs b/{{project-name}}/build.rs new file mode 100644 index 0000000..61caab3 --- /dev/null +++ b/{{project-name}}/build.rs @@ -0,0 +1,172 @@ +use std::{ + env, fs, + io::{BufRead as _, BufReader}, + path::PathBuf, + process::{Child, Command, Stdio}, +}; + +use cargo_metadata::{ + Artifact, CompilerMessage, Message, Metadata, MetadataCommand, Package, Target, +}; +use xtask::AYA_BUILD_EBPF; + +/// This crate has a runtime dependency on artifacts produced by the `{{project-name}}-ebpf` crate. +/// This would be better expressed as one or more [artifact-dependencies][bindeps] but issues such +/// as: +/// +/// * https://github.com/rust-lang/cargo/issues/12374 +/// * https://github.com/rust-lang/cargo/issues/12375 +/// * https://github.com/rust-lang/cargo/issues/12385 +/// +/// prevent their use for the time being. +/// +/// This file, along with the xtask crate, allows analysis tools such as `cargo check`, `cargo +/// clippy`, and even `cargo build` to work as users expect. Prior to this file's existence, this +/// crate's undeclared dependency on artifacts from `{{project-name}}-ebpf` would cause build (and +/// `cargo check`, and `cargo clippy`) failures until the user ran certain other commands in the +/// workspace. Conversely, those same tools (e.g. cargo test --no-run) would produce stale results +/// if run naively because they'd make use of artifacts from a previous build of +/// `{{project-name}}-ebpf`. +/// +/// Note that this solution is imperfect: in particular it has to balance correctness with +/// performance; an environment variable is used to replace true builds of `{{project-name}}-ebpf` +/// with stubs to preserve the property that code generation and linking (in +/// `{{project-name}}-ebpf`) do not occur on metadata-only actions such as `cargo check` or `cargo +/// clippy` of this crate. This means that naively attempting to `cargo test --no-run` this crate +/// will produce binaries that fail at runtime because the stubs are inadequate for actually running +/// the tests. +/// +/// [bindeps]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html?highlight=feature#artifact-dependencies +fn main() { + println!("cargo:rerun-if-env-changed={}", AYA_BUILD_EBPF); + + let build_ebpf = env::var(AYA_BUILD_EBPF) + .as_deref() + .map(str::parse) + .map(Result::unwrap) + .unwrap_or_default(); + + let Metadata { packages, .. } = MetadataCommand::new().no_deps().exec().unwrap(); + let ebpf_package = packages + .into_iter() + .find(|Package { name, .. }| name == "{{project-name}}-ebpf") + .unwrap(); + + let out_dir = env::var_os("OUT_DIR").unwrap(); + let out_dir = PathBuf::from(out_dir); + + let endian = env::var_os("CARGO_CFG_TARGET_ENDIAN").unwrap(); + let target = if endian == "big" { + "bpfeb" + } else if endian == "little" { + "bpfel" + } else { + panic!("unsupported endian={:?}", endian) + }; + + if build_ebpf { + let arch = env::var_os("CARGO_CFG_TARGET_ARCH").unwrap(); + + let target = format!("{target}-unknown-none"); + + let Package { manifest_path, .. } = ebpf_package; + let ebpf_dir = manifest_path.parent().unwrap(); + + // We have a build-dependency on `{{project-name}}-ebpf`, so cargo will automatically rebuild us + // if `{{project-name}}-ebpf`'s *library* target or any of its dependencies change. Since we + // depend on `{{project-name}}-ebpf`'s *binary* targets, that only gets us half of the way. This + // stanza ensures cargo will rebuild us on changes to the binaries too, which gets us the + // rest of the way. + println!("cargo:rerun-if-changed={}", ebpf_dir.as_str()); + + let mut cmd = Command::new("cargo"); + cmd.args([ + "build", + "-Z", + "build-std=core", + "--bins", + "--message-format=json", + "--release", + "--target", + &target, + ]); + + cmd.env("CARGO_CFG_BPF_TARGET_ARCH", arch); + + // Workaround to make sure that the rust-toolchain.toml is respected. + for key in ["RUSTUP_TOOLCHAIN", "RUSTC"] { + cmd.env_remove(key); + } + cmd.current_dir(ebpf_dir); + + // Workaround for https://github.com/rust-lang/cargo/issues/6412 where cargo flocks itself. + let ebpf_target_dir = out_dir.join("{{project-name}}-ebpf"); + cmd.arg("--target-dir").arg(&ebpf_target_dir); + + let mut child = cmd + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap_or_else(|err| panic!("failed to spawn {cmd:?}: {err}")); + let Child { stdout, stderr, .. } = &mut child; + + // Trampoline stdout to cargo warnings. + let stderr = stderr.take().unwrap(); + let stderr = BufReader::new(stderr); + let stderr = std::thread::spawn(move || { + for line in stderr.lines() { + let line = line.unwrap(); + println!("cargo:warning={line}"); + } + }); + + let stdout = stdout.take().unwrap(); + let stdout = BufReader::new(stdout); + let mut executables = Vec::new(); + for message in Message::parse_stream(stdout) { + #[allow(clippy::collapsible_match)] + match message.expect("valid JSON") { + Message::CompilerArtifact(Artifact { + executable, + target: Target { name, .. }, + .. + }) => { + if let Some(executable) = executable { + executables.push((name, executable.into_std_path_buf())); + } + } + Message::CompilerMessage(CompilerMessage { message, .. }) => { + for line in message.rendered.unwrap_or_default().split('\n') { + println!("cargo:warning={line}"); + } + } + Message::TextLine(line) => { + println!("cargo:warning={line}"); + } + _ => {} + } + } + + let status = child + .wait() + .unwrap_or_else(|err| panic!("failed to wait for {cmd:?}: {err}")); + assert_eq!(status.code(), Some(0), "{cmd:?} failed: {status:?}"); + + stderr.join().map_err(std::panic::resume_unwind).unwrap(); + + for (name, binary) in executables { + let dst = out_dir.join(name); + let _: u64 = fs::copy(&binary, &dst) + .unwrap_or_else(|err| panic!("failed to copy {binary:?} to {dst:?}: {err}")); + } + } else { + let Package { targets, .. } = ebpf_package; + for Target { name, kind, .. } in targets { + if *kind != ["bin"] { + continue; + } + let dst = out_dir.join(name); + fs::write(&dst, []).unwrap_or_else(|err| panic!("failed to create {dst:?}: {err}")); + } + } +} diff --git a/{{project-name}}/src/main.rs b/{{project-name}}/src/main.rs index 312bbd6..15103ea 100644 --- a/{{project-name}}/src/main.rs +++ b/{{project-name}}/src/main.rs @@ -8,43 +8,45 @@ use aya::{programs::FExit, Btf}; {%- when "uprobe", "uretprobe" -%} use aya::programs::UProbe; {%- when "sock_ops" -%} -use aya::programs::{SockOps, links::CgroupAttachMode}; +use aya::programs::{links::CgroupAttachMode, SockOps}; {%- when "sk_msg" -%} -use aya::maps::SockHash; -use aya::programs::SkMsg; +use aya::{maps::SockHash, programs::SkMsg}; use {{crate_name}}_common::SockKey; {%- when "xdp" -%} -use anyhow::Context; +use anyhow::Context as _; use aya::programs::{Xdp, XdpFlags}; {%- when "classifier" -%} use aya::programs::{tc, SchedClassifier, TcAttachType}; {%- when "cgroup_skb" -%} -use aya::programs::{CgroupSkb, CgroupSkbAttachType, links::CgroupAttachMode}; +use aya::programs::{links::CgroupAttachMode, CgroupSkb, CgroupSkbAttachType}; {%- when "cgroup_sysctl" -%} -use aya::programs::{CgroupSysctl, links::CgroupAttachMode}; +use aya::programs::{links::CgroupAttachMode, CgroupSysctl}; {%- when "cgroup_sockopt" -%} -use aya::programs::{CgroupSockopt, links::CgroupAttachMode}; +use aya::programs::{links::CgroupAttachMode, CgroupSockopt}; {%- when "tracepoint" -%} use aya::programs::TracePoint; {%- when "lsm" -%} use aya::{programs::Lsm, Btf}; {%- when "perf_event" -%} -use aya::programs::{perf_event, PerfEvent}; -use aya::util::online_cpus; +use aya::{ + programs::{perf_event, PerfEvent}, + util::online_cpus, +}; {%- when "tp_btf" -%} use aya::{programs::BtfTracePoint, Btf}; {%- when "socket_filter" -%} use std::net::TcpStream; + use aya::programs::SocketFilter; {%- when "raw_tracepoint" -%} use aya::programs::RawTracePoint; {%- endcase %} -use aya::{include_bytes_aligned, Ebpf}; -use aya_log::EbpfLogger; {% if program_types_with_opts contains program_type -%} use clap::Parser; {% endif -%} -use log::{info, warn, debug}; + +#[rustfmt::skip] +use log::{debug, info, warn}; use tokio::signal; {% if program_types_with_opts contains program_type -%} @@ -64,7 +66,7 @@ struct Opt { {% endif -%} #[tokio::main] -async fn main() -> Result<(), anyhow::Error> { +async fn main() -> anyhow::Result<()> { {%- if program_types_with_opts contains program_type %} let opt = Opt::parse(); {% endif %} @@ -85,15 +87,11 @@ async fn main() -> Result<(), anyhow::Error> { // 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 ebpf = Ebpf::load(include_bytes_aligned!( - "../../target/bpfel-unknown-none/debug/{{project-name}}" - ))?; - #[cfg(not(debug_assertions))] - let mut ebpf = Ebpf::load(include_bytes_aligned!( - "../../target/bpfel-unknown-none/release/{{project-name}}" - ))?; - if let Err(e) = EbpfLogger::init(&mut ebpf) { + let mut ebpf = aya::Ebpf::load(aya::include_bytes_aligned!(concat!( + env!("OUT_DIR"), + "/{{project-name}}" + )))?; + if let Err(e) = aya_log::EbpfLogger::init(&mut ebpf) { // This can happen if you remove all log statements from your eBPF program. warn!("failed to initialize eBPF logger: {}", e); } @@ -122,7 +120,7 @@ async fn main() -> Result<(), anyhow::Error> { program.load()?; program.attach(cgroup, CgroupAttachMode::default())?; {%- when "sk_msg" -%} - let sock_map: SockHash::<_, SockKey> = ebpf.map("{{sock_map}}").unwrap().try_into()?; + let sock_map: SockHash<_, SockKey> = ebpf.map("{{sock_map}}").unwrap().try_into()?; let map_fd = sock_map.fd().try_clone()?; let prog: &mut SkMsg = ebpf.program_mut("{{crate_name}}").unwrap().try_into()?; @@ -145,7 +143,11 @@ async fn main() -> Result<(), anyhow::Error> { let program: &mut CgroupSkb = ebpf.program_mut("{{crate_name}}").unwrap().try_into()?; let cgroup = std::fs::File::open(opt.cgroup_path)?; program.load()?; - program.attach(cgroup, CgroupSkbAttachType::{{direction}}, CgroupAttachMode::default())?; + program.attach( + cgroup, + CgroupSkbAttachType::{{direction}}, + CgroupAttachMode::default(), + )?; {%- when "tracepoint" -%} let program: &mut TracePoint = ebpf.program_mut("{{crate_name}}").unwrap().try_into()?; program.load()?;