diff --git a/Cargo.lock b/Cargo.lock index d278911..d927a08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -128,6 +128,22 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aya" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758d57288601ecc9d149e3413a5f23d6b72c0373febc97044d4f4aa149033b5e" +dependencies = [ + "bitflags 1.3.2", + "bytes", + "lazy_static", + "libc", + "log", + "object 0.28.4", + "parking_lot", + "thiserror", +] + [[package]] name = "aya" version = "0.13.1" @@ -139,7 +155,7 @@ dependencies = [ "bytes", "libc", "log", - "object", + "object 0.36.7", "once_cell", "thiserror", ] @@ -153,7 +169,7 @@ dependencies = [ "core-error", "hashbrown 0.15.4", "log", - "object", + "object 0.36.7", "thiserror", ] @@ -167,7 +183,7 @@ dependencies = [ "cfg-if", "libc", "miniz_oxide", - "object", + "object 0.36.7", "rustc-demangle", "windows-targets 0.52.6", ] @@ -535,6 +551,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "digest" version = "0.10.7" @@ -611,7 +638,7 @@ name = "eclipta-cli" version = "1.0.0" dependencies = [ "anyhow", - "aya", + "aya 0.13.1", "byteorder", "bytes", "chrono", @@ -627,6 +654,7 @@ dependencies = [ "humantime", "log", "nix", + "object 0.32.2", "prettytable", "serde", "serde_json", @@ -708,6 +736,16 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4742a071cd9694fc86f9fa1a08fa3e53d40cc899d7ee532295da2d085639fbc5" +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "flume" version = "0.11.1" @@ -1339,6 +1377,36 @@ dependencies = [ "libm", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.28.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42c982f2d955fac81dd7e1d0e1426a7d702acd9c98d19ab01083a6a0328c424" +dependencies = [ + "memchr", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "flate2", + "memchr", + "ruzstd", +] + [[package]] name = "object" version = "0.36.7" @@ -1679,6 +1747,17 @@ version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +[[package]] +name = "ruzstd" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c4eb8a81997cf040a091d1f7e1938aeab6749d3a0dfa73af43cdc32393483d" +dependencies = [ + "byteorder", + "derive_more", + "twox-hash", +] + [[package]] name = "ryu" version = "1.0.20" @@ -2068,6 +2147,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "stringprep" version = "0.1.5" @@ -2227,6 +2312,7 @@ dependencies = [ "bytes", "libc", "mio 1.0.4", + "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", @@ -2337,6 +2423,16 @@ dependencies = [ "unicode-width 0.1.14", ] +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "static_assertions", +] + [[package]] name = "typenum" version = "1.18.0" @@ -2420,6 +2516,12 @@ checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" [[package]] name = "user" version = "0.1.0" +dependencies = [ + "anyhow", + "aya 0.11.0", + "num_cpus", + "tokio", +] [[package]] name = "utf8_iter" diff --git a/ebpf-demo/user/Cargo.toml b/ebpf-demo/user/Cargo.toml index 68f345f..52ad8dc 100644 --- a/ebpf-demo/user/Cargo.toml +++ b/ebpf-demo/user/Cargo.toml @@ -4,3 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +aya = "0.11" +anyhow = "1" +tokio = { version = "1", features = ["full"] } +num_cpus = "1" \ No newline at end of file diff --git a/eclipta-cli/Cargo.toml b/eclipta-cli/Cargo.toml index 7e8ed11..7fde05b 100644 --- a/eclipta-cli/Cargo.toml +++ b/eclipta-cli/Cargo.toml @@ -13,7 +13,7 @@ console = "0.15" nix = { version = "0.30", features = ["user", "signal", "process", "resource"] } anyhow = "1" log = "0.4" -tokio = { version = "1.38", features = ["rt-multi-thread", "time", "signal", "fs", "io-util", "macros" ] } +tokio = { version = "1.38", features = ["rt-multi-thread", "time", "signal", "fs", "io-util", "macros" , "process"] } byteorder = "1" bytes = "1.10.1" serde_json = "1.0" @@ -30,4 +30,5 @@ sqlx = { version = "0.7.4", features = ["postgres", "runtime-tokio-rustls", "chr dotenvy = "0.15" uuid = { version = "1.8", features = ["v4"] } colored = "2.1" -prettytable = "0.10.0" \ No newline at end of file +prettytable = "0.10.0" +object = "0.32" diff --git a/eclipta-cli/src/commands/ebpf/load.rs b/eclipta-cli/src/commands/ebpf/load.rs index 60e298a..1764c94 100644 --- a/eclipta-cli/src/commands/ebpf/load.rs +++ b/eclipta-cli/src/commands/ebpf/load.rs @@ -1,170 +1,265 @@ -use aya::{programs::TracePoint, EbpfLoader}; use clap::Args; use std::path::PathBuf; -use crate::utils::logger::{info, success, error}; -use crate::utils::paths::{default_bin_object, default_pin_prefix, default_state_path}; -use crate::utils::state::{AttachmentRecord, load_state, save_state}; -use nix::sys::resource::{setrlimit, Resource, RLIM_INFINITY}; -use nix::unistd::Uid; +use crate::db::programs::{get_program_by_id, get_program_by_title}; +use crate::utils::db::init_db; +// Fixed imports based on current Aya API +use aya::{Ebpf, programs::{Program, ProgramError}}; +use object::Object; +use object::ObjectSection; +use std::collections::HashSet; +use std::io::Error as IoError; +// Import EBUSY from nix crate +use nix::errno::Errno::EBUSY; #[derive(Args, Debug)] pub struct LoadOptions { - /// Path to eBPF ELF (defaults to $ECLIPTA_BIN or ./bin/ebpf.so) #[arg(short, long)] pub program: Option, - /// Program name inside ELF - #[arg(short, long, default_value = "cpu_usage")] - pub name: String, - - /// Tracepoint in the form "category:name" or "category/name" (e.g., "sched:sched_switch") - #[arg(short = 't', long, default_value = "sched:sched_switch")] - pub tracepoint: String, - - /// Pin the program and maps under a prefix in bpffs - #[arg(long, default_value_t = true)] - pub pin: bool, - - /// Pin prefix in bpffs (default $ECLIPTA_PIN_PATH or /sys/fs/bpf/eclipta) #[arg(long)] - pub pin_prefix: Option, + pub id: Option, - /// Persist loader state to this file (default XDG local data dir) #[arg(long)] - pub state_file: Option, + pub title: Option, +} - #[arg(long)] - pub dry_run: bool, +pub const XDP_SECTION: &str = "xdp"; +pub const XDP_DROP_SECTION: &str = "xdp_drop"; +pub const TC_INGRESS_SECTION: &str = "tc_ingress"; +pub const TC_EGRESS_SECTION: &str = "tc_egress"; +pub const SOCKET_FILTER_SECTION: &str = "socket_filter"; +pub const TRACEPOINT_NET_SECTION: &str = "tracepoint/net"; +pub const KPROBE_NET_SECTION: &str = "kprobe/net"; +pub const UPROBE_NET_SECTION: &str = "uprobe/net"; +pub const LSM_NET_SECTION: &str = "lsm/net"; - #[arg(short, long)] - pub verbose: bool, +pub async fn handle_load(opts: LoadOptions) { + let pool = match init_db().await { + Ok(pool) => pool, + Err(e) => { + eprintln!("Failed to init DB: {}", e); + return; + } + }; - #[arg(long)] - pub json: bool, + if let Some(id) = opts.id { + match get_program_by_id(&pool, id).await { + Ok(Some(p)) => { + println!("ID: {}, Title: {}", p.id, p.title); + + handle_file_process(p.path.clone().into()); + } + Ok(None) => println!("No program found with id {}", id), + Err(e) => eprintln!("Failed to fetch program by id {}: {}", id, e), + } + } else if let Some(ref title) = opts.title { + match get_program_by_title(&pool, title).await { + Ok(rows) if rows.len() == 1 => { + let p = &rows[0]; + println!("ID: {}, Title: {}", p.id, p.title); + } + Ok(rows) if rows.len() > 1 => { + eprintln!("Multiple programs found with title '{}'. Please load using --id.", title); + } + Ok(_) => println!("No program found with title '{}'", title), + Err(e) => eprintln!("Failed to fetch programs by title '{}': {}", title, e), + } + } else { + eprintln!("Please specify a program to load using --id or --title"); + } } -pub fn handle_load(opts: LoadOptions) { - let program_path = opts.program.unwrap_or_else(default_bin_object); - if !program_path.exists() { - error(&format!("eBPF ELF file not found at: {}", program_path.display())); - return; +pub fn validate_ebpf_file(path: PathBuf) -> Result<(), String> { + if !path.exists() { + return Err(format!("File does not exist: {}", path.display())); } - if !Uid::effective().is_root() { - error("This command must be run as root to create BPF maps and attach programs. Try: sudo eclipta load ..."); - return; + if !path.is_file() { + return Err(format!("Path is not a file: {}", path.display())); } - // Raise memlock limit to avoid map allocation failures on older kernels - let _ = setrlimit(Resource::RLIMIT_MEMLOCK, RLIM_INFINITY, RLIM_INFINITY); - - // Parse tracepoint category/name - let (tp_category, tp_name) = { - let s = opts.tracepoint.replace('/', ":"); - let mut parts = s.splitn(2, ':'); - let cat = parts.next().unwrap_or("").trim().to_string(); - let nam = parts.next().unwrap_or("").trim().to_string(); - if cat.is_empty() || nam.is_empty() { - error("Tracepoint must be in the form 'category:name' (e.g., 'sched:sched_switch')"); - return; - } - (cat, nam) + if path.extension().and_then(|ext| ext.to_str()) != Some("o") { + return Err(format!("File is not an eBPF object (.o) file: {}", path.display())); + } + + // 2. ELF format validation + let file_data = match std::fs::read(&path) { + Ok(data) => data, + Err(e) => return Err(format!("Failed to read file: {}", e)), }; - if opts.dry_run { - success("✓ Dry run mode - ELF validated and options parsed."); - if opts.verbose { - info(&format!( - "Program '{}' from '{}' would attach to '{}:{}' (pin: {})", - opts.name, - program_path.display(), - tp_category, - tp_name, - opts.pin - )); + let obj = match object::File::parse(&*file_data) { + Ok(obj) => obj, + Err(e) => return Err(format!("Failed to parse ELF file: {}", e)), + }; + + // 3. Section recognition + let mut found_sections = HashSet::new(); + for section in obj.sections() { + if let Ok(name) = section.name() { + match name { + XDP_SECTION | XDP_DROP_SECTION => { found_sections.insert("XDP"); } + TC_INGRESS_SECTION => { found_sections.insert("TC Ingress"); } + TC_EGRESS_SECTION => { found_sections.insert("TC Egress"); } + SOCKET_FILTER_SECTION => { found_sections.insert("Socket Filter"); } + TRACEPOINT_NET_SECTION => { found_sections.insert("Tracepoint"); } + KPROBE_NET_SECTION => { found_sections.insert("Kprobe"); } + UPROBE_NET_SECTION => { found_sections.insert("Uprobe"); } + LSM_NET_SECTION => { found_sections.insert("LSM"); } + _ => {} + } } - return; } - if opts.verbose { - info(&format!("Loading program: {}", opts.name)); - info(&format!("From ELF file: {}", program_path.display())); - info(&format!("Target tracepoint: {}:{}", tp_category, tp_name)); + if found_sections.is_empty() { + return Err("No recognized eBPF program sections found".to_string()); } - let pin_prefix = opts.pin_prefix.unwrap_or_else(default_pin_prefix); - let state_file = opts.state_file.unwrap_or_else(default_state_path); - - match EbpfLoader::new().load_file(&program_path) { - Ok(mut bpf) => { - match bpf.program_mut(&opts.name) { - Some(prog) => { - if let Ok(tp) = prog.try_into() { - let tp: &mut TracePoint = tp; - if let Err(e) = tp.load() { - error(&format!("Failed to load program: {}", e)); - return; - } - - if let Err(e) = tp.attach(&tp_category, &tp_name) { - error(&format!("Failed to attach to tracepoint: {}", e)); - return; - } - - let mut pinned_prog = None; - let mut pinned_maps = Vec::new(); - if opts.pin { - let _ = std::fs::create_dir_all(&pin_prefix); - // Pin program - let prog_pin = pin_prefix.join(&opts.name); - if let Err(e) = tp.pin(&prog_pin) { - error(&format!("Failed to pin program: {}", e)); - } else { - pinned_prog = Some(prog_pin); - } - // Pin maps - for (map_name, m) in bpf.maps_mut() { - let path = pin_prefix.join(map_name); - if let Err(e) = m.pin(&path) { - if opts.verbose { info(&format!("Map '{}' pin failed: {}", map_name, e)); } - } else { - pinned_maps.push(path); - } - } - } - - // Save state - let mut st = load_state(&state_file); - st.attachments.push(AttachmentRecord { - name: opts.name.clone(), - kind: "tracepoint".to_string(), - trace_category: Some(tp_category.clone()), - trace_name: Some(tp_name.clone()), - pinned_prog, - pinned_maps, - pid: std::process::id(), - created_at: chrono::Utc::now().timestamp(), - }); - let _ = save_state(&state_file, st); - - if opts.json { - println!( - "{{ \"status\": \"ok\", \"program\": \"{}\", \"tracepoint\": \"{}:{}\", \"pinned\": {} }}", - opts.name, tp_category, tp_name, opts.pin - ); - } else { - success(&format!("✓ Program '{}' attached to '{}:{}'", opts.name, tp_category, tp_name)); - if opts.pin { info(&format!("Pinned under {}", pin_prefix.display())); } - } - } else { - error("Could not convert program to TracePoint"); - } + println!("Found eBPF program sections: {}", + found_sections.iter().cloned().collect::>().join(", ")); + + // 4. Aya load test - using Ebpf instead of deprecated Bpf + let mut ebpf = match Ebpf::load_file(&path) { + Ok(ebpf) => ebpf, + Err(e) => return Err(format!("Failed to load eBPF object: {}", e)), + }; + + // 5. Map validation + if ebpf.maps().next().is_none() { + println!("Warning: No maps found in eBPF object"); + } + + // 6. Try to load programs (verifier test) - Fixed iteration approach + for (name, program) in ebpf.programs_mut() { + if let Err(e) = load_program_by_type(program) { + return Err(format!("Verifier rejected program {}: {}", name, e)); + } + } + + // 7. Try to attach programs (if possible) - Fixed iteration approach + for (name, program) in ebpf.programs_mut() { + // This is a simplified attachment test - in practice you'd need to handle + // different program types with appropriate attachment methods + if let Err(e) = try_attach_program(name, program) { + // EBUSY might indicate the program is already attached, which is not a validation failure + if let Some(os_error) = e.raw_os_error() { + if os_error == EBUSY as i32 { + continue; // Skip EBUSY errors } - None => error("Program not found in ELF"), } + return Err(format!("Failed to attach program {}: {}", name, e)); } - Err(e) => { - error(&format!("Failed to load ELF: {}", e)); + } + + // 8. Policy/security check (simplified) + if !is_allowed_program_type(&found_sections) { + return Err("Program contains disallowed program types".to_string()); + } + + println!("eBPF object validation successful: {}", path.display()); + Ok(()) +} + +fn is_allowed_program_type(found_sections: &HashSet<&str>) -> bool { + // Implement your policy checks here + // For example, you might want to disallow certain program types + let disallowed_types: HashSet<&str> = ["LSM"].iter().cloned().collect(); + found_sections.is_disjoint(&disallowed_types) +} + +// Helper function to load programs based on their type +fn load_program_by_type(program: &mut Program) -> Result<(), ProgramError> { + match program { + Program::Xdp(p) => p.load(), + Program::SchedClassifier(p) => p.load(), + Program::TracePoint(p) => p.load(), + Program::KProbe(p) => p.load(), + Program::UProbe(p) => p.load(), + Program::SocketFilter(p) => p.load(), + Program::CgroupSkb(p) => p.load(), + Program::CgroupSock(p) => p.load(), + Program::CgroupSockAddr(p) => p.load(), + Program::CgroupSockopt(p) => p.load(), + Program::CgroupSysctl(p) => p.load(), + Program::CgroupDevice(p) => p.load(), + Program::SockOps(p) => p.load(), + Program::SkMsg(p) => p.load(), + Program::SkLookup(p) => p.load(), + Program::PerfEvent(p) => p.load(), + Program::RawTracePoint(p) => p.load(), + Program::SkSkb(p) => p.load(), + // These program types require additional parameters that we don't have in this context + // We'll skip loading them for now and just print a message + Program::Lsm(_) => { + println!("Skipping LSM program load - requires lsm_hook_name and BTF"); + Ok(()) + }, + Program::BtfTracePoint(_) => { + println!("Skipping BTF TracePoint program load - requires tracepoint name and BTF"); + Ok(()) + }, + Program::FEntry(_) => { + println!("Skipping FEntry program load - requires function name and BTF"); + Ok(()) + }, + Program::FExit(_) => { + println!("Skipping FExit program load - requires function name and BTF"); + Ok(()) + }, + Program::Extension(_) => { + println!("Skipping Extension program load - requires ProgramFd and function name"); + Ok(()) + }, + _ => { + // For any program types not explicitly handled + println!("Unknown program type, skipping load"); + Ok(()) } } } + +// Fixed function signature and implementation +fn try_attach_program(name: &str, program: &mut Program) -> Result<(), IoError> { + // This is a simplified example - actual attachment logic would depend on program type + // For now, we'll just return Ok to avoid compilation errors + // In a real implementation, you'd match on program type and attach appropriately + match program { + Program::Xdp(_) => { + // For XDP programs, you'd typically attach to a network interface + // program.attach("eth0", XdpFlags::default())?; + println!("Would attach XDP program: {}", name); + } + Program::SchedClassifier(_) => { + // For TC programs, you'd attach to a network interface with specific parameters + println!("Would attach TC program: {}", name); + } + Program::TracePoint(_) => { + // For tracepoint programs, you'd attach to specific kernel tracepoints + println!("Would attach TracePoint program: {}", name); + } + Program::KProbe(_) => { + // For kprobe programs, you'd attach to specific kernel functions + println!("Would attach KProbe program: {}", name); + } + Program::UProbe(_) => { + // For uprobe programs, you'd attach to specific user-space functions + println!("Would attach UProbe program: {}", name); + } + Program::Lsm(_) => { + // For LSM programs, you'd attach to specific LSM hooks + println!("Would attach LSM program: {}", name); + } + _ => { + println!("Unknown program type for: {}", name); + } + } + + Ok(()) +} + +pub fn handle_file_process(path: PathBuf) { + match validate_ebpf_file(path.clone()) { + Ok(()) => println!("eBPF object file is valid: {}", path.display()), + Err(e) => eprintln!("Validation failed: {}", e), + } +} \ No newline at end of file diff --git a/eclipta-cli/src/db/programs.rs b/eclipta-cli/src/db/programs.rs index 5c09c42..c00d8b5 100644 --- a/eclipta-cli/src/db/programs.rs +++ b/eclipta-cli/src/db/programs.rs @@ -61,4 +61,54 @@ pub async fn delete_program(pool: &Pool, program_id: i32) -> Result<() .await?; Ok(()) -} \ No newline at end of file +} + +pub async fn get_program_by_id( + pool: &Pool, + program_id: i32, +) -> Result, sqlx::Error> { + let row = sqlx::query_as!( + Program, + r#" + SELECT + id, + title, + version, + status, + path + FROM ebpf_programs + WHERE id = $1 + "#, + program_id + ) + .fetch_optional(pool) + .await?; + + Ok(row) +} + +pub async fn get_program_by_title( + pool: &Pool, + title: &str, +) -> Result, sqlx::Error> { + let rows = sqlx::query_as!( + Program, + r#" + SELECT + id, + title, + version, + status, + path + FROM ebpf_programs + WHERE title = $1 + ORDER BY created_at DESC + "#, + title + ) + .fetch_all(pool) + .await?; + + Ok(rows) +} + diff --git a/eclipta-cli/src/main.rs b/eclipta-cli/src/main.rs index 2039829..fb5cd14 100644 --- a/eclipta-cli/src/main.rs +++ b/eclipta-cli/src/main.rs @@ -2,6 +2,7 @@ mod commands; mod utils; mod db; + use clap::{Parser, Subcommand}; // SYSTEM COMMANDS @@ -87,7 +88,7 @@ async fn handle_command(cmd: Commands) -> Result<(), Box> match cmd { Commands::Welcome => run_welcome(), Commands::Status => run_status(), - Commands::Load(opts) => handle_load(opts), + Commands::Load(opts) => handle_load(opts).await, Commands::Unload(opts) => handle_unload(opts), Commands::Inspect(opts) => handle_inspect(opts), Commands::Logs(opts) => handle_logs(opts).await, diff --git a/eclipta-cli/src/utils/db.rs b/eclipta-cli/src/utils/db.rs index 34cbdac..6ba51fe 100644 --- a/eclipta-cli/src/utils/db.rs +++ b/eclipta-cli/src/utils/db.rs @@ -3,16 +3,12 @@ use dotenvy::dotenv; use std::env; use crate::utils::logger::success; - pub type DbPool = Pool; pub async fn init_db() -> Result { - dotenv().ok(); - let db_url = env::var("DATABASE_URL") - .expect("DATABASE_URL must be set in .env"); - + dotenv().ok(); + let db_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set in .env"); let pool = PgPool::connect(&db_url).await?; - run_migrations(&pool).await?; Ok(pool) } @@ -27,7 +23,11 @@ async fn run_migrations(pool: &Pool) -> Result<(), sqlx::Error> { version TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'deactive', path TEXT NOT NULL, + program_id INT, + map_ids INT[], + pinned_path TEXT, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT unique_title_version UNIQUE (title, version) ) "# @@ -35,6 +35,39 @@ async fn run_migrations(pool: &Pool) -> Result<(), sqlx::Error> { .execute(pool) .await?; + sqlx::query( + r#" + CREATE OR REPLACE FUNCTION set_updated_at() + RETURNS TRIGGER AS $$ + BEGIN + NEW.updated_at = NOW(); + RETURN NEW; + END; + $$ LANGUAGE plpgsql; + "# + ) + .execute(pool) + .await?; + + sqlx::query( + r#" + DROP TRIGGER IF EXISTS set_updated_at_trigger ON ebpf_programs; + "# + ) + .execute(pool) + .await?; + + sqlx::query( + r#" + CREATE TRIGGER set_updated_at_trigger + BEFORE UPDATE ON ebpf_programs + FOR EACH ROW + EXECUTE FUNCTION set_updated_at(); + "# + ) + .execute(pool) + .await?; + success("Database migration successful!"); Ok(()) -} \ No newline at end of file +} diff --git a/eclipta-cli/src/utils/paths.rs b/eclipta-cli/src/utils/paths.rs index 78d4876..6342e79 100644 --- a/eclipta-cli/src/utils/paths.rs +++ b/eclipta-cli/src/utils/paths.rs @@ -26,12 +26,12 @@ pub fn default_bin_object() -> PathBuf { cwd.join("bin").join("ebpf.so") } -pub fn default_pin_prefix() -> PathBuf { - if let Ok(p) = env::var("ECLIPTA_PIN_PATH") { - return PathBuf::from(p); - } - PathBuf::from("/sys/fs/bpf/eclipta") -} +// pub fn default_pin_prefix() -> PathBuf { +// if let Ok(p) = env::var("ECLIPTA_PIN_PATH") { +// return PathBuf::from(p); +// } +// PathBuf::from("/sys/fs/bpf/eclipta") +// } pub fn default_state_path() -> PathBuf { if let Ok(p) = env::var("ECLIPTA_STATE") { return PathBuf::from(p); } diff --git a/tests/ebpf_loader b/tests/ebpf_loader new file mode 100755 index 0000000..8ce3ed8 Binary files /dev/null and b/tests/ebpf_loader differ diff --git a/tests/loader.c b/tests/loader.c new file mode 100644 index 0000000..83fe66e --- /dev/null +++ b/tests/loader.c @@ -0,0 +1,237 @@ +// loader.c +// Build: +// gcc -O2 -g loader.c -o ebpf_loader -lbpf -lelf -lz +// +// Run examples: +// sudo ./ebpf_loader /path/to/program.o +// sudo ./ebpf_loader --iface eth0 /path/to/xdp_prog.o +// sudo ./ebpf_loader -i eth0 /path/to/xdp_prog.o +// +// This loader: +// - auto-detects eBPF program sections (xdp, kprobe, tracepoint, uprobe, tc) +// - loads the object into kernel (verifier) +// - attaches programs according to detected section type +// - keeps links alive until SIGINT/SIGTERM + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +static struct bpf_object *g_obj = NULL; +static struct bpf_link **g_links = NULL; +static int g_link_count = 0; +static int g_alloc_links = 0; +static const char *g_iface = NULL; + +static void usage(const char *prog) { + fprintf(stderr, + "Usage: %s [--iface IFACE] \n" + "Options:\n" + " -i, --iface IFACE Interface to attach XDP programs to (eg eth0)\n" + " -h, --help Show this help\n\n" + "Examples:\n" + " sudo %s ./xdp_pass_kern.o --iface eth0\n" + " sudo %s ./trace_prog.o\n", + prog, prog, prog); +} + +static int starts_with(const char *s, const char *pref) { + if (!s || !pref) return 0; + return strncmp(s, pref, strlen(pref)) == 0; +} + +static void free_links_and_obj(void) { + if (g_links) { + for (int i = 0; i < g_link_count; ++i) { + if (g_links[i]) { + bpf_link__destroy(g_links[i]); + g_links[i] = NULL; + } + } + free(g_links); + g_links = NULL; + } + if (g_obj) { + bpf_object__close(g_obj); + g_obj = NULL; + } + g_link_count = 0; + g_alloc_links = 0; +} + +static void handle_sigint(int sig) { + (void)sig; + fprintf(stderr, "\nReceived signal, cleaning up and detaching...\n"); + free_links_and_obj(); + exit(0); +} + +int main(int argc, char **argv) { + const char *path = NULL; + + // parse options with getopt_long + static struct option long_options[] = { + {"iface", required_argument, 0, 'i'}, + {"help", no_argument, 0, 'h'}, + {0,0,0,0} + }; + + int opt; + int option_index = 0; + while ((opt = getopt_long(argc, argv, "i:h", long_options, &option_index)) != -1) { + switch (opt) { + case 'i': + g_iface = optarg; + break; + case 'h': + default: + usage(argv[0]); + return 0; + } + } + + if (optind < argc) { + path = argv[optind]; + } + + if (!path) { + fprintf(stderr, "Error: missing path to .o file\n"); + usage(argv[0]); + return 1; + } + + // register signal handlers for graceful shutdown + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = handle_sigint; + sigaction(SIGINT, &sa, NULL); + sigaction(SIGTERM, &sa, NULL); + + struct bpf_object *obj = NULL; + struct bpf_program *prog; + struct bpf_link *link = NULL; + int err; + + obj = bpf_object__open_file(path, NULL); + if (!obj) { + fprintf(stderr, "failed to open BPF object '%s'\n", path); + return 1; + } + g_obj = obj; // for cleanup on signal + + printf("Detected program sections in %s:\n", path); + bpf_object__for_each_program(prog, obj) { + const char *sec = bpf_program__section_name(prog); + printf(" - section: %s\n", sec ? sec : "(null)"); + } + + err = bpf_object__load(obj); + if (err) { + fprintf(stderr, "failed to load BPF object: %s\n", strerror(-err)); + bpf_object__close(obj); + g_obj = NULL; + return 1; + } + + // allocate link array sized to #programs + int prog_count = 0; + bpf_object__for_each_program(prog, obj) prog_count++; + if (prog_count > 0) { + g_links = calloc(prog_count, sizeof(*g_links)); + if (!g_links) { + fprintf(stderr, "failed to allocate link array\n"); + bpf_object__close(obj); + g_obj = NULL; + return 1; + } + g_alloc_links = prog_count; + } + + // Attach programs based on section prefix + bpf_object__for_each_program(prog, obj) { + const char *sec = bpf_program__section_name(prog); + if (!sec) sec = ""; + + // Get program fd (should be valid after load) + int prog_fd = bpf_program__fd(prog); + if (prog_fd < 0) { + fprintf(stderr, "warning: failed to get fd for program section %s\n", sec); + continue; + } + + if (starts_with(sec, "xdp")) { + if (!g_iface) { + printf("XDP program found (section=%s) but no --iface provided. Skipping attachment.\n", sec); + continue; + } + int ifindex = if_nametoindex(g_iface); + if (ifindex == 0) { + fprintf(stderr, "invalid interface name '%s'\n", g_iface); + continue; + } + + int flags = 0; // change if you want SKB mode: XDP_FLAGS_SKB_MODE + err = bpf_set_link_xdp_fd(ifindex, prog_fd, flags); + if (err < 0) { + fprintf(stderr, "failed to attach XDP program to %s: %s\n", g_iface, strerror(-err)); + } else { + printf("Attached XDP program (section=%s) to iface %s (ifindex=%d)\n", sec, g_iface, ifindex); + // Note: bpf_set_link_xdp_fd does not return a bpf_link object; record placeholder NULL + // so cleanup loop knows this was attached via link-less attach (we cannot bpf_link__destroy it). + // If you prefer to use libbpf's xdp attach helpers (returning bpf_link), swap to that API. + // We'll still keep a placeholder in g_links to maintain indexing. + if (g_link_count < g_alloc_links) g_links[g_link_count++] = NULL; + } + } else if (starts_with(sec, "kprobe") || starts_with(sec, "kretprobe") || + starts_with(sec, "tracepoint") || starts_with(sec, "uprobe") || + starts_with(sec, "uretprobe")) { + link = bpf_program__attach(prog); + if (!link) { + fprintf(stderr, "failed to attach program section %s via libbpf\n", sec); + } else { + printf("Attached program section %s via libbpf (link=%p)\n", sec, (void*)link); + if (g_link_count < g_alloc_links) g_links[g_link_count++] = link; + else { + // shouldn't happen but handle gracefully + bpf_link__destroy(link); + } + } + } else if (starts_with(sec, "tc") || starts_with(sec, "clsact") || starts_with(sec, "classifier")) { + fprintf(stdout, "TC-like section detected (%s). TC attach not implemented by this loader.\n", sec); + // Optionally implement TC attach logic using rtnetlink or libbpf's helper if available. + } else { + // Generic fallback: try libbpf attach + link = bpf_program__attach(prog); + if (!link) { + fprintf(stderr, "fallback attach failed for section %s\n", sec); + } else { + printf("Fallback attached section %s (link=%p)\n", sec, (void*)link); + if (g_link_count < g_alloc_links) g_links[g_link_count++] = link; + else bpf_link__destroy(link); + } + } + } + + printf("All attachments attempted. Active links stored: %d\n", g_link_count); + printf("Loader will keep running to hold programs attached. Press Ctrl-C to exit and detach.\n"); + + // Keep process alive until SIGINT/SIGTERM + while (1) { + pause(); // signal handler will cleanup + } + + // unreachable, kept for completeness + free_links_and_obj(); + return 0; +} diff --git a/tests/xdp_pass_kern.o b/tests/xdp_pass_kern.o new file mode 100644 index 0000000..0a95580 Binary files /dev/null and b/tests/xdp_pass_kern.o differ