Compare commits

...

2 Commits

Author SHA1 Message Date
Claude
71eb4f2429
Ignore process eBPF artifact and Claude worktree directory
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138PxtKH73hqxv7h1oaoSdS
2026-06-17 08:57:58 +00:00
Claude
ed31e34925
Add process-ebpf tracepoint crate for process lifecycle tracking
Adds a new eBPF tracepoint program (sched_process_exec/exit) and a
userspace ProcessTracker that maintains a live pid→comm table via
AsyncPerfEventArray. EbpfServices wires in the tracker; System loads,
attaches, and keeps the process Ebpf handle alive for the duration of
the program.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138PxtKH73hqxv7h1oaoSdS
2026-06-17 08:56:23 +00:00
14 changed files with 350 additions and 5 deletions

2
.gitignore vendored
View File

@ -21,3 +21,5 @@ node_modules/
*.db
lib/ebpf/mantis-ingress
lib/ebpf/mantis-egress
lib/ebpf/mantis-process
.claude/worktrees/

10
Cargo.lock generated
View File

@ -2172,6 +2172,16 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "process-ebpf"
version = "0.1.0"
dependencies = [
"aya-ebpf",
"aya-log-ebpf",
"common",
"which",
]
[[package]]
name = "prost"
version = "0.11.9"

View File

@ -1,6 +1,6 @@
[workspace]
resolver = "2"
members = ["mantis", "common", "macros", "ingress-ebpf", "egress-ebpf"]
members = ["mantis", "common", "macros", "ingress-ebpf", "egress-ebpf", "process-ebpf"]
default-members = ["mantis", "common"]
[workspace.dependencies]
@ -36,3 +36,7 @@ codegen-units = 1
[profile.release.package.egress-ebpf]
debug = 2
codegen-units = 1
[profile.release.package.process-ebpf]
debug = 2
codegen-units = 1

View File

@ -4,4 +4,5 @@ pub mod http_method;
pub mod ip_address;
pub mod packet;
pub mod placeholder;
pub mod process_event;
pub mod pseudo_header;

View File

@ -0,0 +1,15 @@
#[cfg(feature = "user")]
use serde::Serialize;
pub const PROCESS_EVENT_EXEC: u32 = 0;
pub const PROCESS_EVENT_EXIT: u32 = 1;
#[repr(C)]
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "user", derive(Serialize))]
pub struct ProcessEvent {
pub event_type: u32,
pub pid: u32,
pub uid: u32,
pub comm: [u8; 16],
}

View File

@ -13,7 +13,7 @@ compact_str = { workspace = true }
axum = { version = "0.8", features = ["ws", "macros"] }
tower = { version = "0.5", features = ["util"] }
tower-http = { version = "0.6", features = ["cors"] }
aya = { workspace = true }
aya = { workspace = true, features = ["async_tokio"] }
aya-log = { workspace = true }
network-types = { workspace = true }
crossbeam = "0.8.4"

View File

@ -20,6 +20,7 @@ fn main() {
let onnxruntime_dir = lib_dir.join("onnxruntime").join("libonnxruntime.so");
let ingress_edpf_dir = lib_dir.join("ebpf").join("mantis-ingress");
let egress_edpf_dir = lib_dir.join("ebpf").join("mantis-egress");
let process_ebpf_dir = lib_dir.join("ebpf").join("mantis-process");
let static_web = manifest_dir.join("static").join("web");
let project_name = manifest_dir.file_name().unwrap().to_string_lossy().into_owned();
@ -33,6 +34,7 @@ fn main() {
println!("cargo:rustc-env=ONNXRUNTIME_PATH={}", onnxruntime_dir.display());
println!("cargo:rustc-env=INGRESS_PATH={}", ingress_edpf_dir.display());
println!("cargo:rustc-env=EGRESS_PATH={}", egress_edpf_dir.display());
println!("cargo:rustc-env=PROCESS_PATH={}", process_ebpf_dir.display());
println!("cargo:rustc-env=RULE_PATH={}", rule_dir.display());
println!("cargo:rustc-env=DB_PATH={}", db_dir.display());
println!("cargo:rustc-env=RULE_EVE_PATH={}", suricata_eve_socket.display());
@ -42,6 +44,7 @@ fn main() {
// git clean, fresh clone, or manual deletion.
println!("cargo:rerun-if-changed={}", ingress_edpf_dir.display());
println!("cargo:rerun-if-changed={}", egress_edpf_dir.display());
println!("cargo:rerun-if-changed={}", process_ebpf_dir.display());
for item in &[
"src",
@ -57,7 +60,7 @@ fn main() {
}
if env::var_os("SKIP_EBPF_BUILD").is_some() {
for path in &[&ingress_edpf_dir, &egress_edpf_dir] {
for path in &[&ingress_edpf_dir, &egress_edpf_dir, &process_ebpf_dir] {
if !path.exists() {
fs::write(path, []).unwrap_or_else(|e| panic!("cannot write stub {path:?}: {e}"));
}
@ -66,6 +69,7 @@ fn main() {
}
build_ingress_ebpf(&ingress_edpf_dir);
build_egress_ebpf(&egress_edpf_dir);
build_process_ebpf(&process_ebpf_dir);
build_frontend(&frontend_dir, &static_web);
}
@ -278,6 +282,110 @@ fn build_egress_ebpf(dst: &PathBuf) {
}
}
fn build_process_ebpf(dst: &PathBuf) {
let Metadata { packages, .. } = MetadataCommand::new().no_deps().exec().unwrap();
let ebpf_package = packages
.into_iter()
.find(|Package { name, .. }| **name == "process-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)
};
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();
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);
for key in ["RUSTUP_TOOLCHAIN", "RUSTC", "RUSTC_WORKSPACE_WRAPPER"] {
cmd.env_remove(key);
}
cmd.current_dir(ebpf_dir);
let ebpf_target_dir = out_dir.join("../process-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;
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!("{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!("{line}");
}
}
Message::TextLine(line) => {
println!("{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 _: u64 = fs::copy(&binary, dst).unwrap_or_else(|err| panic!("failed to copy {binary:?} to {dst:?}: {err}"));
}
}
fn build_frontend(frontend_dir: &PathBuf, static_dir: &PathBuf) {
if !frontend_dir.exists() {
panic!("Frontend directory {:?} does not exist", frontend_dir);

View File

@ -1,4 +1,5 @@
pub mod access_control;
pub mod process_tracker;
pub mod statistics;
pub mod xsk_manager;
@ -10,6 +11,7 @@ use macros::log;
use tokio::sync::oneshot;
use crate::core::ebpf::access_control::AccessControl;
use crate::core::ebpf::process_tracker::ProcessTracker;
use crate::core::ebpf::statistics::Statistics;
use crate::core::ebpf::xsk_manager::XskManager;
use crate::core::infrastructure::app_config::AppConfig;
@ -22,18 +24,26 @@ pub struct EbpfServices {
pub xsk_manager: Arc<XskManager>,
pub access_control: Arc<AccessControl>,
pub statistics: Arc<Statistics>,
pub process_tracker: Arc<ProcessTracker>,
pub shutdowns: SegQueue<oneshot::Sender<()>>,
}
impl EbpfServices {
pub fn new(app_config: Arc<AppConfig>, ingress_ebpf: &mut Ebpf, egress_ebpf: &mut Ebpf) -> Result<Self, Error> {
pub fn new(
app_config: Arc<AppConfig>,
ingress_ebpf: &mut Ebpf,
egress_ebpf: &mut Ebpf,
process_ebpf: &mut Ebpf,
) -> Result<Self, Error> {
let xsk_manager = XskManager::new(app_config.clone(), ingress_ebpf, egress_ebpf)?;
let access_control = AccessControl::new(ingress_ebpf, egress_ebpf)?;
let statistics = Statistics::new(app_config.clone(), ingress_ebpf, egress_ebpf)?;
let process_tracker = ProcessTracker::new(process_ebpf)?;
let ebpf_services = Self {
xsk_manager: Arc::new(xsk_manager),
access_control: Arc::new(access_control),
statistics: Arc::new(statistics),
process_tracker: Arc::new(process_tracker),
shutdowns: SegQueue::new(),
};
Ok(ebpf_services)

View File

@ -0,0 +1,99 @@
use std::collections::HashMap;
use std::sync::Arc;
use aya::maps::perf::AsyncPerfEventArray;
use aya::maps::MapData;
use aya::util::online_cpus;
use aya::Ebpf;
use bytes::BytesMut;
use common::model::process_event::{ProcessEvent, PROCESS_EVENT_EXEC, PROCESS_EVENT_EXIT};
use macros::log;
use std::sync::Mutex;
use tokio::task;
use crate::model::error::ebpf::EbpfError;
use crate::model::error::Error;
use crate::model::log::ebpf::EbpfLog;
#[derive(Debug, Clone)]
pub struct ProcessInfo {
pub comm: String,
pub uid: u32,
}
pub struct ProcessTracker {
pub table: Arc<Mutex<HashMap<u32, ProcessInfo>>>,
}
impl ProcessTracker {
pub fn new(process_ebpf: &mut Ebpf) -> Result<Self, Error> {
let map = process_ebpf.take_map("PROCESS_EVENTS").ok_or(EbpfError::MapNotFound)?;
let perf_array: AsyncPerfEventArray<MapData> =
AsyncPerfEventArray::try_from(map).map_err(EbpfError::MapOperationError)?;
let table: Arc<Mutex<HashMap<u32, ProcessInfo>>> = Arc::new(Mutex::new(HashMap::new()));
let table_clone = table.clone();
task::spawn(async move {
if let Err(e) = run_perf_reader(perf_array, table_clone).await {
log!(EbpfLog::ProcessTrackerError { error: format!("{e:?}") });
}
});
Ok(Self { table })
}
pub fn lookup(&self, pid: u32) -> Option<ProcessInfo> {
self.table.lock().ok()?.get(&pid).cloned()
}
}
async fn run_perf_reader(
mut perf_array: AsyncPerfEventArray<MapData>,
table: Arc<Mutex<HashMap<u32, ProcessInfo>>>,
) -> Result<(), EbpfError> {
let cpus = online_cpus().map_err(|_| EbpfError::UnknownError)?;
let mut tasks = Vec::new();
for cpu_id in cpus {
let mut buf = perf_array.open(cpu_id, None).map_err(EbpfError::MapOperationError)?;
let table = table.clone();
tasks.push(task::spawn(async move {
let mut buffers: Vec<BytesMut> = (0..10).map(|_| BytesMut::with_capacity(256)).collect();
loop {
let events = match buf.read_events(&mut buffers).await {
Ok(e) => e,
Err(_) => break,
};
for buf in buffers.iter().take(events.read) {
if buf.len() < core::mem::size_of::<ProcessEvent>() {
continue;
}
let event = unsafe { &*(buf.as_ptr() as *const ProcessEvent) };
let comm = String::from_utf8_lossy(
event.comm.iter().take_while(|&&b| b != 0).cloned().collect::<Vec<_>>().as_slice(),
)
.into_owned();
if let Ok(mut t) = table.lock() {
match event.event_type {
PROCESS_EVENT_EXEC => {
t.insert(event.pid, ProcessInfo { comm, uid: event.uid });
}
PROCESS_EVENT_EXIT => {
t.remove(&event.pid);
}
_ => {}
}
}
}
}
}));
}
for t in tasks {
let _ = t.await;
}
Ok(())
}

View File

@ -3,7 +3,7 @@ use std::sync::Arc;
use axum::Router;
use aya::Ebpf;
use aya::maps::{MapData, ProgramArray};
use aya::programs::{Xdp, XdpFlags};
use aya::programs::{TracePoint, Xdp, XdpFlags};
use aya_log::EbpfLogger;
use common::define::program_array::*;
use macros::log;
@ -33,6 +33,7 @@ pub struct System {
pub app_services: Arc<AppServices>,
pub ingress_ebpf: Ebpf,
pub egress_ebpf: Ebpf,
pub process_ebpf: Ebpf,
#[allow(dead_code)]
ingress_program_array: ProgramArray<MapData>,
#[allow(dead_code)]
@ -48,6 +49,7 @@ impl System {
let (mut ingress_ebpf, ingress_program_array) = System::get_ingress_ebpf()?;
let (mut egress_ebpf, egress_program_array) = System::get_egress_ebpf()?;
let mut process_ebpf = System::get_process_ebpf()?;
let app_config = Arc::new(AppConfig::new()?);
let inference_config = Arc::new(InferenceConfig::load_file(
@ -59,6 +61,7 @@ impl System {
app_config.clone(),
&mut ingress_ebpf,
&mut egress_ebpf,
&mut process_ebpf,
)?);
let app_services = Arc::new(AppServices::new(
@ -74,6 +77,7 @@ impl System {
app_services,
ingress_ebpf,
egress_ebpf,
process_ebpf,
ingress_program_array,
egress_program_array,
};
@ -94,6 +98,7 @@ impl System {
self.aya_log_init()?;
self.attach_ebpf()?;
self.attach_process_ebpf()?;
ebpf_services
.run(app_services.ml_engine.clone(), app_services.suricata_engine.clone())
@ -120,6 +125,7 @@ impl System {
fn aya_log_init(&mut self) -> Result<(), Error> {
EbpfLogger::init(&mut self.ingress_ebpf).map_err(EbpfError::LoggerInitFailed)?;
EbpfLogger::init(&mut self.egress_ebpf).map_err(EbpfError::LoggerInitFailed)?;
EbpfLogger::init(&mut self.process_ebpf).map_err(EbpfError::LoggerInitFailed)?;
Ok(())
}
@ -191,6 +197,25 @@ impl System {
Ok(())
}
fn get_process_ebpf() -> Result<Ebpf, Error> {
let ebpf = Ebpf::load(aya::include_bytes_aligned!(env!("PROCESS_PATH"))).map_err(EbpfError::EbpfNotFound)?;
Ok(ebpf)
}
fn attach_process_ebpf(&mut self) -> Result<(), Error> {
for name in &["sched_process_exec", "sched_process_exit"] {
let program: &mut TracePoint = self
.process_ebpf
.program_mut(name)
.ok_or(EbpfError::ProgramNotFound)?
.try_into()
.map_err(EbpfError::GetProgramFailed)?;
program.load().map_err(EbpfError::LoadProgramFailed)?;
program.attach("sched", name).map_err(EbpfError::AttachProgramFailed)?;
}
Ok(())
}
fn get_ingress_ebpf() -> Result<(Ebpf, ProgramArray<MapData>), Error> {
let mut ingress_ebpf =
Ebpf::load(aya::include_bytes_aligned!(env!("INGRESS_PATH"))).map_err(EbpfError::EbpfNotFound)?;

View File

@ -59,5 +59,8 @@ loggable! {
#[error("SO_PREFER_BUSY_POLL not supported on this kernel/driver (queue {queue_id}, errno {errno})")]
BusyPollUnavailable { queue_id: u32, errno: i32 } => tracing::Level::WARN,
#[error("Process tracker error: {error}")]
ProcessTrackerError { error: String } => tracing::Level::ERROR,
}
}

19
process-ebpf/Cargo.toml Normal file
View File

@ -0,0 +1,19 @@
[package]
name = "process-ebpf"
version = "0.1.0"
edition = "2024"
[dependencies]
common = { path = "../common", features = ["kernel"] }
aya-ebpf = { workspace = true }
aya-log-ebpf = { workspace = true }
[build-dependencies]
which = "8.0.0"
[[bin]]
name = "mantis-process"
path = "src/main.rs"
test = false
doctest = false
bench = false

6
process-ebpf/build.rs Normal file
View File

@ -0,0 +1,6 @@
use which::which;
fn main() {
let bpf_linker = which("bpf-linker").unwrap();
println!("cargo:rerun-if-changed={}", bpf_linker.to_str().unwrap());
}

43
process-ebpf/src/main.rs Normal file
View File

@ -0,0 +1,43 @@
#![no_std]
#![no_main]
use aya_ebpf::helpers::{bpf_get_current_comm, bpf_get_current_pid_tgid, bpf_get_current_uid_gid};
use aya_ebpf::macros::{map, tracepoint};
use aya_ebpf::maps::PerfEventArray;
use aya_ebpf::programs::TracePointContext;
use common::model::process_event::{ProcessEvent, PROCESS_EVENT_EXEC, PROCESS_EVENT_EXIT};
#[map]
static PROCESS_EVENTS: PerfEventArray<ProcessEvent> = PerfEventArray::new(0);
#[tracepoint]
pub fn sched_process_exec(ctx: TracePointContext) -> u32 {
emit_event(&ctx, PROCESS_EVENT_EXEC)
}
#[tracepoint]
pub fn sched_process_exit(ctx: TracePointContext) -> u32 {
emit_event(&ctx, PROCESS_EVENT_EXIT)
}
fn emit_event(ctx: &TracePointContext, event_type: u32) -> u32 {
let pid_tgid = unsafe { bpf_get_current_pid_tgid() };
let uid_gid = unsafe { bpf_get_current_uid_gid() };
let mut comm = [0u8; 16];
let _ = unsafe { bpf_get_current_comm(&mut comm) };
let event = ProcessEvent {
event_type,
pid: (pid_tgid >> 32) as u32,
uid: uid_gid as u32,
comm,
};
unsafe { PROCESS_EVENTS.output(ctx, &event, 0) };
0
}
#[cfg(not(test))]
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
unsafe { core::hint::unreachable_unchecked() }
}