From 7eb0de049632124bed2e2a75caef031642cbd700 Mon Sep 17 00:00:00 2001 From: fullzer4 Date: Fri, 2 Jan 2026 12:59:05 -0300 Subject: [PATCH 1/3] wip: nix packages with wrapped daemon --- .envrc | 2 +- crates/leeward-cli/src/main.rs | 106 +++- crates/leeward-core/Cargo.toml | 1 + crates/leeward-core/src/config.rs | 17 +- crates/leeward-core/src/isolation/cgroups.rs | 177 +++++- crates/leeward-core/src/isolation/landlock.rs | 96 ++- crates/leeward-core/src/isolation/mounts.rs | 207 ++++++- crates/leeward-core/src/isolation/seccomp.rs | 85 ++- crates/leeward-core/src/lib.rs | 1 + .../src/protocol.rs | 3 +- crates/leeward-core/src/worker.rs | 214 ++++--- crates/leeward-daemon/src/main.rs | 24 +- crates/leeward-daemon/src/server.rs | 3 +- docs/install.md | 278 +++++++++ flake.lock | 269 +-------- flake.nix | 40 +- ideia.md | 549 ++++++++++++++++++ nix/module.nix | 90 ++- nix/packages.nix | 89 ++- nix/shell.nix | 19 - 20 files changed, 1853 insertions(+), 417 deletions(-) rename crates/{leeward-daemon => leeward-core}/src/protocol.rs (97%) create mode 100644 docs/install.md create mode 100644 ideia.md delete mode 100644 nix/shell.nix diff --git a/.envrc b/.envrc index ff5954f..8392d15 100644 --- a/.envrc +++ b/.envrc @@ -1 +1 @@ -use flake . --impure \ No newline at end of file +use flake \ No newline at end of file diff --git a/crates/leeward-cli/src/main.rs b/crates/leeward-cli/src/main.rs index 71db126..19f93d1 100644 --- a/crates/leeward-cli/src/main.rs +++ b/crates/leeward-cli/src/main.rs @@ -3,6 +3,41 @@ use clap::{Parser, Subcommand}; use leeward_core::config::default_socket_path; use std::path::PathBuf; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::UnixStream; + +/// Send request to daemon and receive response +async fn send_request( + socket_path: &PathBuf, + request: &leeward_core::protocol::Request, +) -> Result> { + // Connect to daemon + let mut stream = UnixStream::connect(socket_path).await?; + + // Encode request + let request_bytes = leeward_core::protocol::encode(request)?; + + // Send length prefix (4 bytes, big-endian) + let len = request_bytes.len() as u32; + stream.write_all(&len.to_be_bytes()).await?; + + // Send request + stream.write_all(&request_bytes).await?; + + // Read response length + let mut len_buf = [0u8; 4]; + stream.read_exact(&mut len_buf).await?; + let response_len = u32::from_be_bytes(len_buf) as usize; + + // Read response + let mut response_buf = vec![0u8; response_len]; + stream.read_exact(&mut response_buf).await?; + + // Decode response + let response = leeward_core::protocol::decode(&response_buf)?; + + Ok(response) +} #[derive(Parser)] #[command(name = "leeward")] @@ -84,22 +119,77 @@ async fn main() -> Result<(), Box> { memory, } => { let socket = socket.unwrap_or_else(default_socket_path); - println!("Executing via daemon at {:?}", socket); - println!("Code: {}", code); - println!("Timeout: {}s, Memory: {}MB", timeout, memory); - // TODO: Connect to daemon and execute + + let request = leeward_core::protocol::Request::Execute( + leeward_core::protocol::ExecuteRequest { + code: Some(code), + shm_slot_id: None, + timeout: Some(std::time::Duration::from_secs(timeout)), + memory_limit: Some(memory * 1024 * 1024), + files: Vec::new(), + } + ); + + match send_request(&socket, &request).await? { + leeward_core::protocol::Response::Execute(resp) => { + if resp.success { + if let Some(result) = resp.result { + print!("{}", String::from_utf8_lossy(&result.stdout)); + eprint!("{}", String::from_utf8_lossy(&result.stderr)); + std::process::exit(result.exit_code); + } + } else { + eprintln!("Error: {}", resp.error.unwrap_or_else(|| "Unknown error".into())); + std::process::exit(1); + } + } + leeward_core::protocol::Response::Error { message } => { + eprintln!("Error: {}", message); + std::process::exit(1); + } + _ => { + eprintln!("Unexpected response"); + std::process::exit(1); + } + } } Commands::Status { socket } => { let socket = socket.unwrap_or_else(default_socket_path); - println!("Getting status from {:?}", socket); - // TODO: Connect to daemon and get status + let request = leeward_core::protocol::Request::Status; + + match send_request(&socket, &request).await? { + leeward_core::protocol::Response::Status { total, idle, busy } => { + println!("Workers: {} total, {} idle, {} busy", total, idle, busy); + } + leeward_core::protocol::Response::Error { message } => { + eprintln!("Error: {}", message); + std::process::exit(1); + } + _ => { + eprintln!("Unexpected response"); + std::process::exit(1); + } + } } Commands::Ping { socket } => { let socket = socket.unwrap_or_else(default_socket_path); - println!("Pinging daemon at {:?}", socket); - // TODO: Connect and ping + let request = leeward_core::protocol::Request::Ping; + + match send_request(&socket, &request).await? { + leeward_core::protocol::Response::Pong => { + println!("Pong!"); + } + leeward_core::protocol::Response::Error { message } => { + eprintln!("Error: {}", message); + std::process::exit(1); + } + _ => { + eprintln!("Unexpected response"); + std::process::exit(1); + } + } } Commands::Run { diff --git a/crates/leeward-core/Cargo.toml b/crates/leeward-core/Cargo.toml index a2dd861..24ae038 100644 --- a/crates/leeward-core/Cargo.toml +++ b/crates/leeward-core/Cargo.toml @@ -16,6 +16,7 @@ caps = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } serde = { workspace = true } +rmp-serde = { workspace = true } libc = { workspace = true } memfd = { workspace = true } diff --git a/crates/leeward-core/src/config.rs b/crates/leeward-core/src/config.rs index 5d39f14..d4c2266 100644 --- a/crates/leeward-core/src/config.rs +++ b/crates/leeward-core/src/config.rs @@ -41,7 +41,7 @@ pub struct SandboxConfig { impl Default for SandboxConfig { fn default() -> Self { Self { - python_path: PathBuf::from("/usr/bin/python3"), + python_path: find_python(), ro_binds: vec![ PathBuf::from("/usr"), PathBuf::from("/lib"), @@ -142,6 +142,21 @@ impl SandboxConfigBuilder { } } +/// Find Python executable in PATH +fn find_python() -> PathBuf { + if let Ok(path_var) = std::env::var("PATH") { + for dir in path_var.split(':') { + for name in &["python3", "python"] { + let candidate = PathBuf::from(dir).join(name); + if candidate.exists() { + return candidate; + } + } + } + } + PathBuf::from("python3") +} + /// Get default socket path from LEEWARD_SOCKET env var or system default /// /// Returns: diff --git a/crates/leeward-core/src/isolation/cgroups.rs b/crates/leeward-core/src/isolation/cgroups.rs index 14c7f1a..1f12182 100644 --- a/crates/leeward-core/src/isolation/cgroups.rs +++ b/crates/leeward-core/src/isolation/cgroups.rs @@ -1,6 +1,9 @@ //! Cgroups v2 resource limits -use crate::Result; +use crate::{LeewardError, Result}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::os::unix::io::{AsRawFd, RawFd}; /// Configuration for cgroups v2 resource limits #[derive(Debug, Clone)] @@ -29,7 +32,6 @@ impl Default for CgroupsConfig { impl CgroupsConfig { /// Create a new cgroup for a sandbox pub fn create_cgroup(&self, name: &str) -> Result { - // TODO: Create cgroup under /sys/fs/cgroup/leeward/{name} tracing::debug!( name, memory = self.memory_max, @@ -37,50 +39,203 @@ impl CgroupsConfig { pids = self.pids_max, "creating cgroup" ); + + let cgroup_root = PathBuf::from("/sys/fs/cgroup"); + let leeward_cgroup = cgroup_root.join("leeward"); + let cgroup_path = leeward_cgroup.join(name); + + // Ensure leeward parent cgroup exists + if !leeward_cgroup.exists() { + fs::create_dir_all(&leeward_cgroup) + .map_err(|e| LeewardError::Cgroups(format!("failed to create leeward cgroup: {e}")))?; + + // Enable controllers in leeward cgroup + enable_controllers(&cgroup_root, "leeward")?; + } + + // Create the specific cgroup + if !cgroup_path.exists() { + fs::create_dir_all(&cgroup_path) + .map_err(|e| LeewardError::Cgroups(format!("failed to create cgroup {}: {e}", name)))?; + } + + // Set memory limit + let memory_max_path = cgroup_path.join("memory.max"); + fs::write(&memory_max_path, self.memory_max.to_string()) + .map_err(|e| LeewardError::Cgroups(format!("failed to set memory.max: {e}")))?; + + // Set memory swap limit (0 = no swap) + if !self.allow_swap { + let swap_max_path = cgroup_path.join("memory.swap.max"); + fs::write(&swap_max_path, "0") + .map_err(|e| LeewardError::Cgroups(format!("failed to set memory.swap.max: {e}")))?; + } + + // Set CPU limit (percentage as quota/period) + // cpu.max format: "$quota $period" or "max $period" + let cpu_max_path = cgroup_path.join("cpu.max"); + let period = 100000; // 100ms in microseconds + let quota = if self.cpu_percent >= 100 { + "max".to_string() + } else { + ((self.cpu_percent as u64 * period) / 100).to_string() + }; + fs::write(&cpu_max_path, format!("{} {}", quota, period)) + .map_err(|e| LeewardError::Cgroups(format!("failed to set cpu.max: {e}")))?; + + // Set PIDs limit + let pids_max_path = cgroup_path.join("pids.max"); + fs::write(&pids_max_path, self.pids_max.to_string()) + .map_err(|e| LeewardError::Cgroups(format!("failed to set pids.max: {e}")))?; + + // Get FD for CLONE_INTO_CGROUP + let cgroup_fd = fs::File::open(&cgroup_path) + .map_err(|e| LeewardError::Cgroups(format!("failed to open cgroup: {e}")))? + .as_raw_fd(); + Ok(CgroupHandle { name: name.to_string(), - path: format!("/sys/fs/cgroup/leeward/{name}"), + path: cgroup_path, + fd: Some(cgroup_fd), }) } } +/// Enable controllers in a cgroup +fn enable_controllers(parent: &Path, child_name: &str) -> Result<()> { + let subtree_control = parent.join("cgroup.subtree_control"); + + // Read current controllers + let current = fs::read_to_string(&subtree_control).unwrap_or_default(); + + // Enable memory, cpu, and pids controllers if not already enabled + let mut controllers = vec![]; + if !current.contains("memory") { + controllers.push("+memory"); + } + if !current.contains("cpu") { + controllers.push("+cpu"); + } + if !current.contains("pids") { + controllers.push("+pids"); + } + + if !controllers.is_empty() { + let control_str = controllers.join(" "); + fs::write(&subtree_control, control_str) + .map_err(|e| LeewardError::Cgroups(format!( + "failed to enable controllers for {}: {e}", + child_name + )))?; + } + + Ok(()) +} + /// Handle to a cgroup #[derive(Debug)] pub struct CgroupHandle { name: String, - path: String, + path: PathBuf, + fd: Option, } impl CgroupHandle { + /// Get the file descriptor for CLONE_INTO_CGROUP + pub fn as_raw_fd(&self) -> Option { + self.fd + } + /// Add a process to this cgroup pub fn add_process(&self, pid: u32) -> Result<()> { - // TODO: Write pid to cgroup.procs tracing::debug!(cgroup = %self.name, pid, "adding process to cgroup"); + + let procs_path = self.path.join("cgroup.procs"); + fs::write(&procs_path, pid.to_string()) + .map_err(|e| LeewardError::Cgroups(format!("failed to add process to cgroup: {e}")))?; + Ok(()) } /// Get current memory usage pub fn memory_current(&self) -> Result { - // TODO: Read memory.current - Ok(0) + let memory_current_path = self.path.join("memory.current"); + let content = fs::read_to_string(&memory_current_path) + .map_err(|e| LeewardError::Cgroups(format!("failed to read memory.current: {e}")))?; + + content + .trim() + .parse() + .map_err(|e| LeewardError::Cgroups(format!("failed to parse memory.current: {e}"))) } /// Get peak memory usage pub fn memory_peak(&self) -> Result { - // TODO: Read memory.peak - Ok(0) + let memory_peak_path = self.path.join("memory.peak"); + let content = fs::read_to_string(&memory_peak_path) + .map_err(|e| LeewardError::Cgroups(format!("failed to read memory.peak: {e}")))?; + + content + .trim() + .parse() + .map_err(|e| LeewardError::Cgroups(format!("failed to parse memory.peak: {e}"))) } /// Check if OOM killed pub fn was_oom_killed(&self) -> Result { - // TODO: Read memory.events for oom_kill + let events_path = self.path.join("memory.events"); + let content = fs::read_to_string(&events_path) + .map_err(|e| LeewardError::Cgroups(format!("failed to read memory.events: {e}")))?; + + // Parse events file for oom_kill count + for line in content.lines() { + if let Some(oom_line) = line.strip_prefix("oom_kill ") { + let count: u64 = oom_line + .parse() + .map_err(|e| LeewardError::Cgroups(format!("failed to parse oom_kill count: {e}")))?; + return Ok(count > 0); + } + } + Ok(false) } + /// Get CPU usage statistics + pub fn cpu_stat(&self) -> Result<(u64, u64)> { + let cpu_stat_path = self.path.join("cpu.stat"); + let content = fs::read_to_string(&cpu_stat_path) + .map_err(|e| LeewardError::Cgroups(format!("failed to read cpu.stat: {e}")))?; + + let mut usage_usec = 0u64; + let mut user_usec = 0u64; + + for line in content.lines() { + if let Some(usage) = line.strip_prefix("usage_usec ") { + usage_usec = usage.parse() + .map_err(|e| LeewardError::Cgroups(format!("failed to parse usage_usec: {e}")))?; + } else if let Some(user) = line.strip_prefix("user_usec ") { + user_usec = user.parse() + .map_err(|e| LeewardError::Cgroups(format!("failed to parse user_usec: {e}")))?; + } + } + + Ok((usage_usec, user_usec)) + } + /// Destroy the cgroup pub fn destroy(self) -> Result<()> { - // TODO: rmdir the cgroup tracing::debug!(cgroup = %self.name, "destroying cgroup"); + + // Close the file descriptor first if we have one + if let Some(fd) = self.fd { + // SAFETY: closing a file descriptor we own + unsafe { libc::close(fd); } + } + + // Remove the cgroup directory + fs::remove_dir(&self.path) + .map_err(|e| LeewardError::Cgroups(format!("failed to remove cgroup: {e}")))?; + Ok(()) } } diff --git a/crates/leeward-core/src/isolation/landlock.rs b/crates/leeward-core/src/isolation/landlock.rs index 202a79e..0ec3bb5 100644 --- a/crates/leeward-core/src/isolation/landlock.rs +++ b/crates/leeward-core/src/isolation/landlock.rs @@ -1,7 +1,11 @@ //! Landlock filesystem sandboxing -use crate::{LeewardError, Result}; +use crate::Result; use std::path::PathBuf; +use landlock::{ + Access, AccessFs, Ruleset, RulesetAttr, RulesetCreatedAttr, + RulesetStatus, ABI +}; /// Configuration for Landlock filesystem restrictions #[derive(Debug, Clone, Default)] @@ -38,13 +42,101 @@ impl LandlockConfig { /// Apply Landlock restrictions to the current process pub fn apply(&self) -> Result<()> { - // TODO: Implement using landlock crate tracing::debug!( ro = self.ro_paths.len(), rw = self.rw_paths.len(), exec = self.exec_paths.len(), "applying landlock rules" ); + + // Check if Landlock is supported - use V2 for now (Linux 5.19+) + let abi = ABI::V2; + tracing::debug!("Using Landlock ABI version: {:?}", abi); + + // Create ruleset with all filesystem access flags we want to control + let mut ruleset = Ruleset::default() + .handle_access(AccessFs::from_all(abi)) + .map_err(|e| crate::LeewardError::Landlock(format!("failed to create ruleset: {e}")))? + .create() + .map_err(|e| crate::LeewardError::Landlock(format!("failed to create ruleset: {e}")))?; + + // Add read-only paths + let ro_access = AccessFs::ReadFile | AccessFs::ReadDir; + for path in &self.ro_paths { + if path.exists() { + let file = std::fs::File::open(path) + .map_err(|e| crate::LeewardError::Landlock(format!("failed to open {}: {e}", path.display())))?; + ruleset = ruleset + .add_rule(landlock::PathBeneath::new(file, ro_access)) + .map_err(|e| crate::LeewardError::Landlock(format!( + "failed to add ro rule for {}: {e}", + path.display() + )))?; + tracing::debug!("added read-only access for {}", path.display()); + } + } + + // Add read-write paths + let rw_access = AccessFs::ReadFile + | AccessFs::WriteFile + | AccessFs::ReadDir + | AccessFs::RemoveDir + | AccessFs::RemoveFile + | AccessFs::MakeChar + | AccessFs::MakeDir + | AccessFs::MakeReg + | AccessFs::MakeSock + | AccessFs::MakeFifo + | AccessFs::MakeBlock + | AccessFs::MakeSym; + + for path in &self.rw_paths { + if path.exists() { + let file = std::fs::File::open(path) + .map_err(|e| crate::LeewardError::Landlock(format!("failed to open {}: {e}", path.display())))?; + ruleset = ruleset + .add_rule(landlock::PathBeneath::new(file, rw_access)) + .map_err(|e| crate::LeewardError::Landlock(format!( + "failed to add rw rule for {}: {e}", + path.display() + )))?; + tracing::debug!("added read-write access for {}", path.display()); + } + } + + // Add execute paths + let exec_access = AccessFs::Execute | AccessFs::ReadFile; + for path in &self.exec_paths { + if path.exists() { + let file = std::fs::File::open(path) + .map_err(|e| crate::LeewardError::Landlock(format!("failed to open {}: {e}", path.display())))?; + ruleset = ruleset + .add_rule(landlock::PathBeneath::new(file, exec_access)) + .map_err(|e| crate::LeewardError::Landlock(format!( + "failed to add exec rule for {}: {e}", + path.display() + )))?; + tracing::debug!("added execute access for {}", path.display()); + } + } + + // Enforce the ruleset + let status = ruleset + .restrict_self() + .map_err(|e| crate::LeewardError::Landlock(format!("failed to enforce landlock: {e}")))?; + + match status.ruleset { + RulesetStatus::NotEnforced => { + tracing::warn!("Landlock ruleset could not be enforced"); + } + RulesetStatus::PartiallyEnforced => { + tracing::info!("Landlock ruleset partially enforced"); + } + RulesetStatus::FullyEnforced => { + tracing::info!("Landlock ruleset fully enforced"); + } + } + Ok(()) } } diff --git a/crates/leeward-core/src/isolation/mounts.rs b/crates/leeward-core/src/isolation/mounts.rs index b0af559..3c2de14 100644 --- a/crates/leeward-core/src/isolation/mounts.rs +++ b/crates/leeward-core/src/isolation/mounts.rs @@ -2,6 +2,8 @@ use crate::{LeewardError, Result}; use std::path::PathBuf; +use std::ffi::CString; +use std::os::unix::ffi::OsStrExt; /// Configuration for filesystem mounts #[derive(Debug, Clone, Default)] @@ -48,19 +50,55 @@ impl MountConfig { } fn setup_root(&self) -> Result<()> { - // TODO: Create new root directory structure tracing::debug!(root = ?self.new_root, "setting up root"); + + // Create new root if it doesn't exist + if self.new_root != PathBuf::new() { + std::fs::create_dir_all(&self.new_root) + .map_err(|e| LeewardError::Mount(format!("failed to create new root: {e}")))?; + + // Create essential directories + for dir in &["proc", "sys", "dev", "tmp", "home", "home/sandbox"] { + let path = self.new_root.join(dir); + std::fs::create_dir_all(&path) + .map_err(|e| LeewardError::Mount(format!("failed to create {}: {e}", dir)))?; + } + } + Ok(()) } fn setup_binds(&self) -> Result<()> { for (src, dst) in &self.ro_binds { tracing::debug!(?src, ?dst, "ro bind mount"); - // TODO: mount --bind, then remount ro + + if src.exists() { + // Ensure destination exists + if let Some(parent) = dst.parent() { + std::fs::create_dir_all(parent) + .map_err(|e| LeewardError::Mount(format!("failed to create mount point: {e}")))?; + } + + // Bind mount + mount_bind(src, dst)?; + // Remount read-only + mount_remount_ro(dst)?; + } } + for (src, dst) in &self.rw_binds { tracing::debug!(?src, ?dst, "rw bind mount"); - // TODO: mount --bind + + if src.exists() { + // Ensure destination exists + if let Some(parent) = dst.parent() { + std::fs::create_dir_all(parent) + .map_err(|e| LeewardError::Mount(format!("failed to create mount point: {e}")))?; + } + + // Bind mount + mount_bind(src, dst)?; + } } Ok(()) } @@ -68,15 +106,172 @@ impl MountConfig { fn setup_tmpfs(&self) -> Result<()> { for (path, size) in &self.tmpfs { tracing::debug!(?path, size, "tmpfs mount"); - // TODO: mount -t tmpfs -o size={size} + + // Ensure mount point exists + std::fs::create_dir_all(path) + .map_err(|e| LeewardError::Mount(format!("failed to create tmpfs mount point: {e}")))?; + + mount_tmpfs(path, *size)?; } Ok(()) } fn do_pivot_root(&self) -> Result<()> { - // TODO: pivot_root(new_root, put_old) - // Then unmount and remove put_old tracing::debug!(root = ?self.new_root, "pivot_root"); + + if self.new_root == PathBuf::new() { + return Ok(()); // Skip pivot_root if no new root specified + } + + let put_old = self.new_root.join("put_old"); + std::fs::create_dir_all(&put_old) + .map_err(|e| LeewardError::Mount(format!("failed to create put_old: {e}")))?; + + pivot_root(&self.new_root, &put_old)?; + + // Change to new root + std::env::set_current_dir("/") + .map_err(|e| LeewardError::Mount(format!("failed to chdir to /: {e}")))?; + + // Unmount old root + umount2(&PathBuf::from("/put_old"), libc::MNT_DETACH)?; + + // Remove put_old directory + std::fs::remove_dir("/put_old") + .map_err(|e| LeewardError::Mount(format!("failed to remove put_old: {e}")))?; + Ok(()) } } + +// Helper functions for mount operations + +fn path_to_cstring(path: &std::path::Path) -> Result { + CString::new(path.as_os_str().as_bytes()) + .map_err(|e| LeewardError::Mount(format!("invalid path {}: {}", path.display(), e))) +} + +fn mount_bind(src: &std::path::Path, dst: &std::path::Path) -> Result<()> { + let src_c = path_to_cstring(src)?; + let dst_c = path_to_cstring(dst)?; + + // SAFETY: mount syscall with bind flag + let ret = unsafe { + libc::mount( + src_c.as_ptr(), + dst_c.as_ptr(), + std::ptr::null(), + libc::MS_BIND | libc::MS_REC, + std::ptr::null(), + ) + }; + + if ret != 0 { + return Err(LeewardError::Mount(format!( + "failed to bind mount {} to {}: {}", + src.display(), + dst.display(), + std::io::Error::last_os_error() + ))); + } + + Ok(()) +} + +fn mount_remount_ro(path: &std::path::Path) -> Result<()> { + let path_c = path_to_cstring(path)?; + + // SAFETY: mount syscall to remount read-only + let ret = unsafe { + libc::mount( + std::ptr::null(), + path_c.as_ptr(), + std::ptr::null(), + libc::MS_BIND | libc::MS_REMOUNT | libc::MS_RDONLY, + std::ptr::null(), + ) + }; + + if ret != 0 { + return Err(LeewardError::Mount(format!( + "failed to remount {} read-only: {}", + path.display(), + std::io::Error::last_os_error() + ))); + } + + Ok(()) +} + +fn mount_tmpfs(path: &std::path::Path, size: u64) -> Result<()> { + let path_c = path_to_cstring(path)?; + let fstype = CString::new("tmpfs") + .map_err(|e| LeewardError::Mount(format!("invalid fstype: {e}")))?; + + let size_mb = size / (1024 * 1024); + let options = CString::new(format!("size={}M", size_mb)) + .map_err(|e| LeewardError::Mount(format!("invalid options: {e}")))?; + + // SAFETY: mount syscall with tmpfs + let ret = unsafe { + libc::mount( + fstype.as_ptr(), + path_c.as_ptr(), + fstype.as_ptr(), + 0, + options.as_ptr() as *const libc::c_void, + ) + }; + + if ret != 0 { + return Err(LeewardError::Mount(format!( + "failed to mount tmpfs at {}: {}", + path.display(), + std::io::Error::last_os_error() + ))); + } + + Ok(()) +} + +fn pivot_root(new_root: &std::path::Path, put_old: &std::path::Path) -> Result<()> { + let new_root_c = path_to_cstring(new_root)?; + let put_old_c = path_to_cstring(put_old)?; + + // SAFETY: pivot_root syscall + let ret = unsafe { + libc::syscall( + libc::SYS_pivot_root, + new_root_c.as_ptr(), + put_old_c.as_ptr(), + ) + }; + + if ret != 0 { + return Err(LeewardError::Mount(format!( + "pivot_root failed: {}", + std::io::Error::last_os_error() + ))); + } + + Ok(()) +} + +fn umount2(path: &std::path::Path, flags: i32) -> Result<()> { + let path_c = path_to_cstring(path)?; + + // SAFETY: umount2 syscall + let ret = unsafe { + libc::umount2(path_c.as_ptr(), flags) + }; + + if ret != 0 { + return Err(LeewardError::Mount(format!( + "umount2 failed for {}: {}", + path.display(), + std::io::Error::last_os_error() + ))); + } + + Ok(()) +} diff --git a/crates/leeward-core/src/isolation/seccomp.rs b/crates/leeward-core/src/isolation/seccomp.rs index 353d867..d0ab59b 100644 --- a/crates/leeward-core/src/isolation/seccomp.rs +++ b/crates/leeward-core/src/isolation/seccomp.rs @@ -1,7 +1,11 @@ //! Seccomp-BPF syscall filtering with SECCOMP_USER_NOTIF support use crate::{LeewardError, Result}; -use std::os::unix::io::{AsRawFd, RawFd}; +use std::os::unix::io::RawFd; +use std::collections::BTreeMap; +use seccompiler::{ + SeccompAction, SeccompFilter, SeccompRule, TargetArch +}; /// Configuration for seccomp filtering #[derive(Debug, Clone)] @@ -31,25 +35,68 @@ impl SeccompConfig { /// seccomp notifications. The supervisor can poll this fd and decide /// what to do with blocked syscalls. pub fn apply(&self) -> Result> { - // TODO: Build and apply BPF filter using seccompiler - // For notify mode: - // 1. Build filter with SECCOMP_RET_USER_NOTIF for blocked syscalls - // 2. Use seccomp(SECCOMP_SET_MODE_FILTER, ...) to apply - // 3. Get notification fd from seccomp(SECCOMP_GET_NOTIF_SIZES, ...) - tracing::debug!( notify = self.notify_mode, syscalls = self.allowed_syscalls.len(), "applying seccomp filter" ); - if self.notify_mode { - // TODO: Return actual notification fd - // For now, return None as placeholder - Ok(None) - } else { - Ok(None) + // Build the filter + let filter = self.build_filter()?; + + // Apply the filter + // Note: SECCOMP_USER_NOTIF requires kernel 5.0+ and special handling + // For now, we'll use basic filtering with KILL action for denied syscalls + // Convert filter to BPF program and apply it + let bpf_prog: seccompiler::BpfProgram = filter + .try_into() + .map_err(|e| LeewardError::Seccomp(format!("failed to compile filter to BPF: {e}")))?; + + seccompiler::apply_filter(&bpf_prog) + .map_err(|e| LeewardError::Seccomp(format!("failed to apply seccomp filter: {e}")))?; + + tracing::info!("seccomp filter applied with {} allowed syscalls", self.allowed_syscalls.len()); + + // SECCOMP_USER_NOTIF would require: + // 1. Using raw seccomp() syscall with SECCOMP_FILTER_FLAG_NEW_LISTENER + // 2. Getting notification fd from kernel + // 3. Setting up notification handler thread + // For now, return None as we're using basic filtering + Ok(None) + } + + /// Build the seccomp filter + fn build_filter(&self) -> Result { + let mut rules = BTreeMap::new(); + + // For each allowed syscall, create a rule with Allow action + // SeccompRule::new only takes conditions, the action is Allow by default for matched rules + for &syscall_num in &self.allowed_syscalls { + rules.insert( + syscall_num, + vec![SeccompRule::new(vec![]) + .map_err(|e| LeewardError::Seccomp(format!("failed to create rule: {e}")))?], + ); } + + // Default action for unmatched syscalls + let default_action = if self.log_denials { + SeccompAction::Log // Log and deny + } else { + SeccompAction::KillThread // Kill the thread + }; + + // Get current architecture + let arch = get_arch(); + + // Create the filter + SeccompFilter::new( + rules, + default_action, + SeccompAction::Allow, // Bad architecture action + arch, + ) + .map_err(|e| LeewardError::Seccomp(format!("failed to create seccomp filter: {e}"))) } } @@ -154,6 +201,18 @@ impl Drop for SeccompNotifyFd { } } +/// Get the current architecture for seccomp +fn get_arch() -> TargetArch { + #[cfg(target_arch = "x86_64")] + return TargetArch::x86_64; + + #[cfg(target_arch = "aarch64")] + return TargetArch::aarch64; + + #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] + compile_error!("Unsupported architecture for seccomp"); +} + /// Default syscalls needed for Python to run fn default_python_syscalls() -> Vec { vec![ diff --git a/crates/leeward-core/src/lib.rs b/crates/leeward-core/src/lib.rs index e5018b0..dee6a2b 100644 --- a/crates/leeward-core/src/lib.rs +++ b/crates/leeward-core/src/lib.rs @@ -17,6 +17,7 @@ pub mod config; pub mod error; pub mod isolation; pub mod pipe; +pub mod protocol; pub mod result; pub mod shm; pub mod worker; diff --git a/crates/leeward-daemon/src/protocol.rs b/crates/leeward-core/src/protocol.rs similarity index 97% rename from crates/leeward-daemon/src/protocol.rs rename to crates/leeward-core/src/protocol.rs index 3ca1bab..08e5c5a 100644 --- a/crates/leeward-daemon/src/protocol.rs +++ b/crates/leeward-core/src/protocol.rs @@ -2,9 +2,8 @@ //! //! Supports both traditional msgpack and zero-copy shared memory modes -use leeward_core::ExecutionResult; +use crate::ExecutionResult; use serde::{Deserialize, Serialize}; -use std::os::unix::io::RawFd; use std::time::Duration; /// Request to execute code diff --git a/crates/leeward-core/src/worker.rs b/crates/leeward-core/src/worker.rs index c2d6ca3..54a8bae 100644 --- a/crates/leeward-core/src/worker.rs +++ b/crates/leeward-core/src/worker.rs @@ -1,9 +1,6 @@ -//! Sandbox worker process management - use crate::{pipe::ParentPipe, ExecutionResult, LeewardError, Result, SandboxConfig}; use std::os::unix::io::RawFd; -/// State of a worker in the pool #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WorkerState { /// Ready to accept work @@ -16,33 +13,19 @@ pub enum WorkerState { Dead, } -/// A pre-forked sandboxed worker process -/// -/// In the new paradigm: -/// - Workers are created at daemon startup with clone3 + CLONE_INTO_CGROUP -/// - Python interpreter is already loaded and idle -/// - Code is sent via pipe, execution is immediate (~0.5ms) -/// - Workers survive denied syscalls (SECCOMP_USER_NOTIF) #[derive(Debug)] pub struct Worker { - /// Unique worker ID pub id: u32, - /// Current state pub state: WorkerState, - /// Process ID (if running) pub pid: Option, - /// Number of executions completed pub execution_count: u64, - /// Configuration for this worker config: SandboxConfig, - /// Communication pipe with the worker pipe: Option, /// Cgroup file descriptor (for CLONE_INTO_CGROUP) cgroup_fd: Option, } impl Worker { - /// Create a new worker with the given config pub fn new(id: u32, config: SandboxConfig) -> Self { Self { id, @@ -55,52 +38,65 @@ impl Worker { } } - /// Spawn the worker process using pre-fork model - /// - /// This uses clone3 with CLONE_INTO_CGROUP to create a fully isolated - /// worker process with Python already loaded. pub fn spawn(&mut self) -> Result<()> { - use crate::isolation::clone3; + use crate::isolation::{clone3, CgroupsConfig}; use crate::pipe::WorkerPipe; tracing::info!(worker_id = self.id, "spawning pre-forked worker"); - // Create communication pipes - let worker_pipe = WorkerPipe::new()?; - let (parent_pipe, child_pipe) = worker_pipe.split(); + // Try to create cgroup for this worker (optional - continue if it fails) + let cgroup_fd = if self.config.memory_limit > 0 || self.config.cpu_limit < 100 { + let cgroup_config = CgroupsConfig { + memory_max: self.config.memory_limit, + cpu_percent: self.config.cpu_limit, + pids_max: self.config.max_pids, + allow_swap: false, + }; + + match cgroup_config.create_cgroup(&format!("worker-{}", self.id)) { + Ok(cgroup_handle) => { + let fd = cgroup_handle.as_raw_fd().unwrap_or(-1); + // Store cgroup handle (we'll leak it for now, proper cleanup later) + std::mem::forget(cgroup_handle); + tracing::info!("cgroups enabled for worker {}", self.id); + fd + } + Err(e) => { + tracing::warn!("cgroups not available (running without root?): {}", e); + -1 + } + } + } else { + -1 + }; - // TODO: Create cgroup for this worker and get fd - // For now, use -1 as placeholder (will be implemented with cgroups) - let cgroup_fd = -1; + self.cgroup_fd = Some(cgroup_fd); - // Get namespace flags from config - let namespace_flags = self.config_to_namespace_flags(); + // Create pipes for communication + let worker_pipe = WorkerPipe::new()?; + let (parent_pipe, child_pipe) = worker_pipe.split(); - // Clone config for child process + // Get namespace flags (but don't include them in clone3, we'll set them inside) + let namespace_flags = 0; // We'll enter namespaces from inside the worker let config = self.config.clone(); - // Clone the worker process with full isolation let pid = clone3::clone_worker(cgroup_fd, namespace_flags, move || { - // Child process: Set up isolation and load Python worker_main(child_pipe, &config) })?; - // Parent process: Store worker info self.pid = Some(pid); self.pipe = Some(parent_pipe); - self.cgroup_fd = Some(cgroup_fd); self.state = WorkerState::Idle; tracing::info!( worker_id = self.id, pid = pid, - "worker spawned and ready" + "worker spawned and ready with cgroup" ); Ok(()) } - /// Execute code in this worker via pipe pub fn execute(&mut self, code: &str) -> Result { if self.state != WorkerState::Idle { return Err(LeewardError::Execution(format!( @@ -117,14 +113,12 @@ impl Worker { self.state = WorkerState::Busy; tracing::debug!(worker_id = self.id, code_len = code.len(), "sending code to worker"); - // Send code via pipe pipe.send_code(code.as_bytes())?; + let result_bytes = pipe.recv_result()?; - // Receive result via pipe - let _result_bytes = pipe.recv_result()?; - - // TODO: Deserialize result from MessagePack - let result = ExecutionResult::default(); + // MessagePack deserialization + let result: ExecutionResult = rmp_serde::from_slice(&result_bytes) + .map_err(|e| LeewardError::Execution(format!("failed to deserialize result: {}", e)))?; self.execution_count += 1; self.state = WorkerState::Idle; @@ -138,36 +132,28 @@ impl Worker { Ok(result) } - /// Kill and recycle this worker pub fn recycle(&mut self) -> Result<()> { tracing::info!(worker_id = self.id, "recycling worker"); self.state = WorkerState::Recycling; - // Kill existing process if any if let Some(pid) = self.pid { unsafe { libc::kill(pid, libc::SIGKILL); } } - // Close pipe self.pipe = None; - - // Reset state self.pid = None; self.execution_count = 0; - // Spawn new worker self.spawn() } - /// Check if worker should be recycled based on execution count #[must_use] pub fn should_recycle(&self, max_executions: u64) -> bool { self.execution_count >= max_executions } - /// Convert config to namespace flags fn config_to_namespace_flags(&self) -> u64 { use nix::sched::CloneFlags; @@ -185,26 +171,63 @@ impl Worker { } } -/// Worker main function (runs in child process) -fn worker_main(mut pipe: crate::pipe::ChildPipe, _config: &SandboxConfig) -> Result<()> { - use crate::isolation::{LandlockConfig, SeccompConfig}; +fn worker_main(mut pipe: crate::pipe::ChildPipe, config: &SandboxConfig) -> Result<()> { + use crate::isolation::{LandlockConfig, SeccompConfig, NamespaceConfig}; - tracing::debug!("worker process starting, setting up isolation"); + tracing::debug!("worker process starting isolation setup"); - // Set up Landlock filesystem restrictions - LandlockConfig::default().apply()?; + // Step 1: Setup namespaces (critical for security) + let namespace_config = NamespaceConfig { + user: false, // User namespace needs UID mapping setup + pid: true, // Isolate process tree + mount: true, // Isolate filesystem + net: !config.allow_network, // Network isolation + ipc: true, // IPC isolation + uts: true, // Hostname isolation + }; - // Set up seccomp with NOTIFY mode - let _notify_fd = SeccompConfig::default().apply()?; + namespace_config.enter()?; + tracing::info!("namespaces configured"); - // TODO: Load Python interpreter - tracing::info!("worker isolation complete, loading Python"); + // Step 2: Apply Landlock filesystem restrictions (if available) + // Landlock requires Linux 5.13+, but that's okay - we try it + let mut landlock = LandlockConfig::default(); - // Enter idle loop, waiting for code via pipe - loop { - tracing::debug!("worker waiting for code"); + // Add Python path and libraries as executable + if let Some(python_dir) = config.python_path.parent() { + landlock = landlock.exec(python_dir).ro(python_dir); + } + + // Add read-only paths + for path in &config.ro_binds { + landlock = landlock.ro(path); + } + + // Add read-write paths + for path in &config.rw_binds { + landlock = landlock.rw(path); + } - // Receive code from daemon + // Add /tmp as read-write + landlock = landlock.rw("/tmp"); + + match landlock.apply() { + Ok(_) => tracing::info!("landlock restrictions applied"), + Err(e) => { + // Landlock is nice to have but not critical if we have seccomp + namespaces + tracing::warn!("landlock not available (kernel < 5.13?): {}", e); + } + } + + // Step 3: Apply seccomp filter (critical for security) + let seccomp = SeccompConfig::default(); + seccomp.apply()?; + tracing::info!("seccomp filter applied"); + + tracing::info!("worker fully isolated, entering main loop"); + + // Main worker loop + loop { let code = match pipe.recv_code() { Ok(code) => code, Err(e) => { @@ -213,20 +236,65 @@ fn worker_main(mut pipe: crate::pipe::ChildPipe, _config: &SandboxConfig) -> Res } }; - tracing::debug!(code_len = code.len(), "received code, executing"); + let exec_result = execute_python(&code, config); - // TODO: Execute code in Python - // For now, just echo back - let result = code; + let result_bytes = match rmp_serde::to_vec(&exec_result) { + Ok(bytes) => bytes, + Err(e) => { + tracing::error!("failed to serialize result: {}", e); + break; + } + }; - // Send result back to daemon - if let Err(e) = pipe.send_result(&result) { + if let Err(e) = pipe.send_result(&result_bytes) { tracing::error!("failed to send result: {}", e); break; } - - tracing::debug!("result sent, waiting for next code"); } Ok(()) } + +fn execute_python(code: &[u8], config: &SandboxConfig) -> ExecutionResult { + use std::process::{Command, Stdio}; + use std::time::Instant; + + let code_str = String::from_utf8_lossy(code); + let start = Instant::now(); + + let output = match Command::new(&config.python_path) + .arg("-c") + .arg(code_str.as_ref()) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + { + Ok(output) => output, + Err(e) => { + return ExecutionResult { + exit_code: -1, + stdout: Vec::new(), + stderr: format!("Failed to execute Python: {}", e).into_bytes(), + duration: start.elapsed(), + memory_peak: 0, + cpu_time_us: 0, + timed_out: false, + oom_killed: false, + }; + } + }; + + let duration = start.elapsed(); + + ExecutionResult { + exit_code: output.status.code().unwrap_or(-1), + stdout: output.stdout, + stderr: output.stderr, + duration, + memory_peak: 0, // TODO: Get from cgroup memory.peak + cpu_time_us: 0, // TODO: Get from /proc/[pid]/stat + timed_out: false, // TODO: Implement timeout handling + oom_killed: false, // TODO: Detect from cgroup events + } +} diff --git a/crates/leeward-daemon/src/main.rs b/crates/leeward-daemon/src/main.rs index cd17914..d1a6964 100644 --- a/crates/leeward-daemon/src/main.rs +++ b/crates/leeward-daemon/src/main.rs @@ -13,7 +13,6 @@ use tracing_subscriber::EnvFilter; mod config; mod iouring; mod pool; -mod protocol; mod server; use config::DaemonConfig; @@ -43,6 +42,29 @@ async fn main() -> Result<()> { // Remove existing socket let _ = std::fs::remove_file(&config.socket_path); + // Validate Python + let python_path = &config.sandbox_config.python_path; + + match std::process::Command::new(python_path) + .arg("--version") + .output() + { + Ok(output) => { + let version = String::from_utf8_lossy(&output.stdout) + .trim() + .to_string(); + let version = if version.is_empty() { + String::from_utf8_lossy(&output.stderr).trim().to_string() + } else { + version + }; + tracing::info!(python = ?python_path, version = %version, "Python ready"); + } + Err(e) => { + anyhow::bail!("Python not found or not executable: {}", e); + } + } + // Bind socket let listener = UnixListener::bind(&config.socket_path)?; tracing::info!(socket = ?config.socket_path, "listening"); diff --git a/crates/leeward-daemon/src/server.rs b/crates/leeward-daemon/src/server.rs index f21afd3..6e10506 100644 --- a/crates/leeward-daemon/src/server.rs +++ b/crates/leeward-daemon/src/server.rs @@ -1,6 +1,7 @@ //! Unix socket server -use crate::{config::DaemonConfig, pool::WorkerPool, protocol::{self, Request, Response}}; +use crate::{config::DaemonConfig, pool::WorkerPool}; +use leeward_core::protocol::{self, Request, Response}; use std::sync::Arc; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, diff --git a/docs/install.md b/docs/install.md new file mode 100644 index 0000000..c392b3e --- /dev/null +++ b/docs/install.md @@ -0,0 +1,278 @@ +# Installation Guide + +## Quick Start (No root required) + +leeward works without root, but with reduced isolation (no cgroups resource limits). + +```bash +# Build +cargo build --release + +# Run daemon +./target/release/leeward-daemon & + +# Execute code +./target/release/leeward exec "print('hello')" +``` + +## Full Setup (with cgroups v2) + +For complete isolation with memory/CPU limits (like Docker), you need cgroups v2 configured. + +### 1. Verify cgroups v2 is enabled + +```bash +# Check if cgroups v2 is mounted +mount | grep cgroup2 + +# Should show something like: +# cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime) +``` + +If not mounted, add to kernel boot parameters: +``` +systemd.unified_cgroup_hierarchy=1 +``` + +### 2. Enable cgroup delegation for your user + +#### Option A: Systemd user service (recommended) + +Create `/etc/systemd/system/user@.service.d/delegate.conf`: +```ini +[Service] +Delegate=cpu cpuset io memory pids +``` + +Then reload: +```bash +sudo systemctl daemon-reload +``` + +#### Option B: Run with systemd-run + +```bash +systemd-run --user --scope -p Delegate=yes ./target/release/leeward-daemon +``` + +#### Option C: Configure user cgroup delegation + +```bash +# Add your user to systemd cgroup delegation +sudo mkdir -p /etc/systemd/system/user@$(id -u).service.d/ +sudo tee /etc/systemd/system/user@$(id -u).service.d/delegate.conf << 'EOF' +[Service] +Delegate=cpu cpuset io memory pids +EOF + +sudo systemctl daemon-reload +sudo systemctl restart user@$(id -u).service +``` + +### 3. Verify delegation + +```bash +# Check your user's cgroup +cat /sys/fs/cgroup/user.slice/user-$(id -u).slice/cgroup.controllers +# Should show: cpuset cpu io memory pids +``` + +### 4. Run leeward with full isolation + +```bash +# As root (simplest, for testing) +sudo ./target/release/leeward-daemon + +# Or as user with delegation configured +./target/release/leeward-daemon +``` + +## Distribution-Specific Setup + +### NixOS + +Add to your `configuration.nix`: + +```nix +{ config, pkgs, ... }: +{ + # Enable cgroups v2 (default on modern NixOS) + boot.kernelParams = [ "systemd.unified_cgroup_hierarchy=1" ]; + + # Enable user namespaces + boot.kernel.sysctl."kernel.unprivileged_userns_clone" = 1; + + # Import leeward module + imports = [ + (builtins.fetchTarball "https://github.com/vektia/leeward/archive/main.tar.gz" + "/nix/module.nix") + ]; + + services.leeward = { + enable = true; + # Optional: configure workers, memory limits, etc. + workers = 4; + }; +} +``` + +### Ubuntu/Debian + +```bash +# Enable cgroups v2 (if not already) +sudo sed -i 's/GRUB_CMDLINE_LINUX=""/GRUB_CMDLINE_LINUX="systemd.unified_cgroup_hierarchy=1"/' /etc/default/grub +sudo update-grub +sudo reboot + +# Enable user namespaces +echo 'kernel.unprivileged_userns_clone=1' | sudo tee /etc/sysctl.d/99-userns.conf +sudo sysctl --system + +# Setup cgroup delegation +sudo mkdir -p /etc/systemd/system/user@.service.d/ +sudo tee /etc/systemd/system/user@.service.d/delegate.conf << 'EOF' +[Service] +Delegate=cpu cpuset io memory pids +EOF +sudo systemctl daemon-reload +``` + +### Fedora/RHEL + +```bash +# cgroups v2 is default on Fedora 31+ + +# Enable user namespaces (if disabled) +sudo sysctl -w kernel.unprivileged_userns_clone=1 +echo 'kernel.unprivileged_userns_clone=1' | sudo tee /etc/sysctl.d/99-userns.conf + +# Setup cgroup delegation +sudo mkdir -p /etc/systemd/system/user@.service.d/ +sudo tee /etc/systemd/system/user@.service.d/delegate.conf << 'EOF' +[Service] +Delegate=cpu cpuset io memory pids +EOF +sudo systemctl daemon-reload +``` + +### Arch Linux + +```bash +# cgroups v2 is default + +# Enable cgroup delegation +sudo mkdir -p /etc/systemd/system/user@.service.d/ +sudo tee /etc/systemd/system/user@.service.d/delegate.conf << 'EOF' +[Service] +Delegate=cpu cpuset io memory pids +EOF +sudo systemctl daemon-reload +``` + +## Systemd Service (Production) + +Create `/etc/systemd/system/leeward.service`: + +```ini +[Unit] +Description=Leeward Sandbox Daemon +After=network.target + +[Service] +Type=simple +ExecStart=/usr/local/bin/leeward-daemon +Restart=always +RestartSec=5 + +# Security hardening +NoNewPrivileges=true +ProtectSystem=strict +ProtectHome=true +PrivateTmp=true + +# Cgroup delegation for workers +Delegate=cpu cpuset io memory pids + +# Resource limits for the daemon itself +MemoryMax=1G +CPUQuota=200% + +[Install] +WantedBy=multi-user.target +``` + +Enable and start: +```bash +sudo systemctl enable leeward +sudo systemctl start leeward +``` + +## Docker/Podman (Alternative) + +If you can't configure cgroups on the host, run leeward inside a privileged container: + +```bash +# Docker +docker run -d --privileged --name leeward \ + -v /var/run/leeward:/var/run/leeward \ + ghcr.io/vektia/leeward:latest + +# Podman (rootless with --userns=keep-id) +podman run -d --privileged --userns=keep-id --name leeward \ + -v /var/run/leeward:/var/run/leeward \ + ghcr.io/vektia/leeward:latest +``` + +## Troubleshooting + +### "Permission denied" on cgroups + +```bash +# Check if cgroups v2 is mounted +mount | grep cgroup + +# Check delegation +cat /sys/fs/cgroup/user.slice/user-$(id -u).slice/cgroup.controllers + +# Try running with systemd-run +systemd-run --user --scope -p Delegate=yes ./target/release/leeward-daemon +``` + +### "Operation not permitted" on namespaces + +```bash +# Check if user namespaces are enabled +cat /proc/sys/kernel/unprivileged_userns_clone +# Should be 1 + +# If 0, enable: +sudo sysctl -w kernel.unprivileged_userns_clone=1 +``` + +### Landlock not available + +```bash +# Check kernel version (need >= 5.13) +uname -r + +# Check if Landlock is enabled +cat /sys/kernel/security/lsm +# Should include "landlock" +``` + +## Verification + +After setup, verify everything works: + +```bash +# Start daemon +./target/release/leeward-daemon & + +# Check status +./target/release/leeward status + +# Execute test code +./target/release/leeward exec "import os; print(os.getpid())" + +# Check isolation (should fail - no network in sandbox) +./target/release/leeward exec "import urllib.request; urllib.request.urlopen('http://google.com')" +``` diff --git a/flake.lock b/flake.lock index 88df767..1f011b4 100644 --- a/flake.lock +++ b/flake.lock @@ -1,114 +1,5 @@ { "nodes": { - "cachix": { - "inputs": { - "devenv": [ - "devenv" - ], - "flake-compat": [ - "devenv", - "flake-compat" - ], - "git-hooks": [ - "devenv", - "git-hooks" - ], - "nixpkgs": [ - "devenv", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1760971495, - "narHash": "sha256-IwnNtbNVrlZIHh7h4Wz6VP0Furxg9Hh0ycighvL5cZc=", - "owner": "cachix", - "repo": "cachix", - "rev": "c5bfd933d1033672f51a863c47303fc0e093c2d2", - "type": "github" - }, - "original": { - "owner": "cachix", - "ref": "latest", - "repo": "cachix", - "type": "github" - } - }, - "devenv": { - "inputs": { - "cachix": "cachix", - "flake-compat": "flake-compat", - "flake-parts": "flake-parts", - "git-hooks": "git-hooks", - "nix": "nix", - "nixd": "nixd", - "nixpkgs": "nixpkgs" - }, - "locked": { - "lastModified": 1767288951, - "narHash": "sha256-160ZiJhibIkePTQ3wLjLcPgxseP78sF59psTTm5oLCQ=", - "owner": "cachix", - "repo": "devenv", - "rev": "7f7e03392c9ce626a9ef412d42b3bef2f7f8625e", - "type": "github" - }, - "original": { - "owner": "cachix", - "repo": "devenv", - "type": "github" - } - }, - "flake-compat": { - "flake": false, - "locked": { - "lastModified": 1761588595, - "narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5", - "type": "github" - }, - "original": { - "owner": "edolstra", - "repo": "flake-compat", - "type": "github" - } - }, - "flake-parts": { - "inputs": { - "nixpkgs-lib": [ - "devenv", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1760948891, - "narHash": "sha256-TmWcdiUUaWk8J4lpjzu4gCGxWY6/Ok7mOK4fIFfBuU4=", - "owner": "hercules-ci", - "repo": "flake-parts", - "rev": "864599284fc7c0ba6357ed89ed5e2cd5040f0c04", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "flake-parts", - "type": "github" - } - }, - "flake-root": { - "locked": { - "lastModified": 1723604017, - "narHash": "sha256-rBtQ8gg+Dn4Sx/s+pvjdq3CB2wQNzx9XGFq/JVGCB6k=", - "owner": "srid", - "repo": "flake-root", - "rev": "b759a56851e10cb13f6b8e5698af7b59c44be26e", - "type": "github" - }, - "original": { - "owner": "srid", - "repo": "flake-root", - "type": "github" - } - }, "flake-utils": { "inputs": { "systems": "systems" @@ -127,138 +18,7 @@ "type": "github" } }, - "git-hooks": { - "inputs": { - "flake-compat": [ - "devenv", - "flake-compat" - ], - "gitignore": "gitignore", - "nixpkgs": [ - "devenv", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1760663237, - "narHash": "sha256-BflA6U4AM1bzuRMR8QqzPXqh8sWVCNDzOdsxXEguJIc=", - "owner": "cachix", - "repo": "git-hooks.nix", - "rev": "ca5b894d3e3e151ffc1db040b6ce4dcc75d31c37", - "type": "github" - }, - "original": { - "owner": "cachix", - "repo": "git-hooks.nix", - "type": "github" - } - }, - "gitignore": { - "inputs": { - "nixpkgs": [ - "devenv", - "git-hooks", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1709087332, - "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", - "owner": "hercules-ci", - "repo": "gitignore.nix", - "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "gitignore.nix", - "type": "github" - } - }, - "nix": { - "inputs": { - "flake-compat": [ - "devenv", - "flake-compat" - ], - "flake-parts": [ - "devenv", - "flake-parts" - ], - "git-hooks-nix": [ - "devenv", - "git-hooks" - ], - "nixpkgs": [ - "devenv", - "nixpkgs" - ], - "nixpkgs-23-11": [ - "devenv" - ], - "nixpkgs-regression": [ - "devenv" - ] - }, - "locked": { - "lastModified": 1766922625, - "narHash": "sha256-O0wExzdYqSNqbPYCQhUWeoKlDa7q6wxhuWiHolxqdl8=", - "owner": "cachix", - "repo": "nix", - "rev": "c62c4bdb6673871ae5cdc51c498df6292d5169aa", - "type": "github" - }, - "original": { - "owner": "cachix", - "ref": "devenv-2.32", - "repo": "nix", - "type": "github" - } - }, - "nixd": { - "inputs": { - "flake-parts": [ - "devenv", - "flake-parts" - ], - "flake-root": "flake-root", - "nixpkgs": [ - "devenv", - "nixpkgs" - ], - "treefmt-nix": "treefmt-nix" - }, - "locked": { - "lastModified": 1763964548, - "narHash": "sha256-JTRoaEWvPsVIMFJWeS4G2isPo15wqXY/otsiHPN0zww=", - "owner": "nix-community", - "repo": "nixd", - "rev": "d4bf15e56540422e2acc7bc26b20b0a0934e3f5e", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "nixd", - "type": "github" - } - }, "nixpkgs": { - "locked": { - "lastModified": 1761313199, - "narHash": "sha256-wCIACXbNtXAlwvQUo1Ed++loFALPjYUA3dpcUJiXO44=", - "owner": "cachix", - "repo": "devenv-nixpkgs", - "rev": "d1c30452ebecfc55185ae6d1c983c09da0c274ff", - "type": "github" - }, - "original": { - "owner": "cachix", - "ref": "rolling", - "repo": "devenv-nixpkgs", - "type": "github" - } - }, - "nixpkgs_2": { "locked": { "lastModified": 1761114652, "narHash": "sha256-f/QCJM/YhrV/lavyCVz8iU3rlZun6d+dAiC3H+CDle4=", @@ -274,7 +34,7 @@ "type": "github" } }, - "nixpkgs_3": { + "nixpkgs_2": { "locked": { "lastModified": 1744536153, "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", @@ -292,15 +52,14 @@ }, "root": { "inputs": { - "devenv": "devenv", "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs_2", + "nixpkgs": "nixpkgs", "rust-overlay": "rust-overlay" } }, "rust-overlay": { "inputs": { - "nixpkgs": "nixpkgs_3" + "nixpkgs": "nixpkgs_2" }, "locked": { "lastModified": 1766890375, @@ -330,28 +89,6 @@ "repo": "default", "type": "github" } - }, - "treefmt-nix": { - "inputs": { - "nixpkgs": [ - "devenv", - "nixd", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1734704479, - "narHash": "sha256-MMi74+WckoyEWBRcg/oaGRvXC9BVVxDZNRMpL+72wBI=", - "owner": "numtide", - "repo": "treefmt-nix", - "rev": "65712f5af67234dad91a5a4baee986a8b62dbf8f", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "treefmt-nix", - "type": "github" - } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 6f62a17..0750b34 100644 --- a/flake.nix +++ b/flake.nix @@ -5,10 +5,9 @@ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; rust-overlay.url = "github:oxalica/rust-overlay"; - devenv.url = "github:cachix/devenv"; }; - outputs = inputs@{ self, nixpkgs, flake-utils, rust-overlay, devenv }: + outputs = inputs@{ self, nixpkgs, flake-utils, rust-overlay }: let nixosModules.default = import ./nix/module.nix; in @@ -18,6 +17,11 @@ pkgs = import nixpkgs { inherit system overlays; }; lib = import ./nix/lib.nix { inherit pkgs; }; packages = import ./nix/packages.nix { inherit pkgs lib; }; + + # Rust toolchain + rustToolchain = pkgs.rust-bin.stable.latest.default.override { + extensions = [ "rust-src" "rust-analyzer" ]; + }; in { packages = { @@ -27,9 +31,35 @@ ffi = packages.leeward-ffi; }; - devShells.default = devenv.lib.mkShell { - inherit inputs pkgs; - modules = [ (import ./nix/shell.nix) ]; + devShells.default = pkgs.mkShell { + buildInputs = with pkgs; [ + rustToolchain + cargo-watch + pkg-config + libseccomp + mold + clang + llvmPackages.bintools + python3 + ]; + + shellHook = '' + export LIBSECCOMP_LINK_TYPE="dylib" + export LIBSECCOMP_LIB_PATH="${pkgs.libseccomp}/lib" + export PKG_CONFIG_PATH="${pkgs.libseccomp}/lib/pkgconfig" + export LEEWARD_SOCKET="$PWD/.leeward.sock" + export RUST_SRC_PATH="${rustToolchain}/lib/rustlib/src/rust/library" + + echo "πŸš€ Leeward Development Environment" + echo "" + echo "Comandos:" + echo " cargo build --release - Compila o projeto" + echo " ./target/release/leeward-daemon - Inicia o daemon" + echo " ./target/release/leeward exec 'print(1)' - Executa cΓ³digo" + echo "" + echo "Para cgroups (opcional), rode com:" + echo " sudo ./target/release/leeward-daemon" + ''; }; } ) // { inherit nixosModules; }; diff --git a/ideia.md b/ideia.md new file mode 100644 index 0000000..d97358a --- /dev/null +++ b/ideia.md @@ -0,0 +1,549 @@ +════════════════════════════════════════════════════════════════════════════════ + ARQUITETURA LEEWARD v1 +════════════════════════════════════════════════════════════════════════════════ + + + VISΓƒO GERAL +──────────────────────────────────────────────────────────────────────────────── + +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”‚ +β”‚ CLIENT (Python/Node/Go/Rust) β”‚ +β”‚ β”‚ β”‚ +β”‚ β”‚ 1. Prepara arquivos em /data/jobs/{job_id}/input/ β”‚ +β”‚ β”‚ 2. Chama leeward via FFI β”‚ +β”‚ β”‚ 3. Recebe resultado β”‚ +β”‚ β”‚ 4. LΓͺ output de /data/jobs/{job_id}/output/ β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ libleeward.so (C FFI) β”‚ +β”‚ β”‚ β”‚ +β”‚ β”‚ - Serializa request (msgpack) β”‚ +β”‚ β”‚ - Envia via Unix socket β”‚ +β”‚ β”‚ - Deserializa response β”‚ +β”‚ β”‚ β”‚ +β””β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”‚ Unix socket: /var/run/leeward.sock + β”‚ Protocolo: [4 bytes len][msgpack payload] + β”‚ Tamanho: ~200 bytes (sΓ³ metadata, nΓ£o dados) + β”‚ +β”Œβ”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β–Ό β”‚ +β”‚ DAEMON (leeward-daemon) β”‚ +β”‚ β”‚ β”‚ +β”‚ β”œβ”€β”€ Server (tokio async) β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”œβ”€β”€ Aceita conexΓ΅es β”‚ +β”‚ β”‚ β”œβ”€β”€ Deserializa requests β”‚ +β”‚ β”‚ β”œβ”€β”€ Valida paths (allowed_paths check) β”‚ +β”‚ β”‚ └── Dispatch para worker pool β”‚ +β”‚ β”‚ β”‚ +β”‚ β”œβ”€β”€ Worker Pool β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”œβ”€β”€ [W1] ──┐ β”‚ +β”‚ β”‚ β”œβ”€β”€ [W2] ──┼── Processos pre-forked, isolados, Python quente β”‚ +β”‚ β”‚ β”œβ”€β”€ [W3] ─── β”‚ +β”‚ β”‚ └── [W4] β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ └── Supervisor β”‚ +β”‚ β”‚ β”‚ +β”‚ β”œβ”€β”€ seccomp-notify handler β”‚ +β”‚ β”œβ”€β”€ Timeout enforcer β”‚ +β”‚ └── Metrics collector β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”‚ pipe (cΓ³digo + config, ~200 bytes) + β”‚ +β”Œβ”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β–Ό β”‚ +β”‚ WORKER (processo isolado) β”‚ +β”‚ β”‚ β”‚ +β”‚ β”œβ”€β”€ Namespaces: user, pid, mount, net, ipc, uts β”‚ +β”‚ β”œβ”€β”€ Bind mounts: /input (ro), /output (rw), /usr (ro) β”‚ +β”‚ β”œβ”€β”€ Landlock: sΓ³ acessa paths permitidos β”‚ +β”‚ β”œβ”€β”€ Seccomp: whitelist de ~30 syscalls β”‚ +β”‚ β”œβ”€β”€ Cgroups: memory, cpu, pids limits β”‚ +β”‚ β”‚ β”‚ +β”‚ └── Python interpreter β”‚ +β”‚ β”‚ β”‚ +β”‚ β”œβ”€β”€ pandas, numpy pre-loaded β”‚ +β”‚ β”œβ”€β”€ Executa cΓ³digo do usuΓ‘rio β”‚ +β”‚ └── Retorna stdout/stderr/exit_code β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”‚ bind mount (zero-copy, mesmo inode) + β”‚ +β”Œβ”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β–Ό β”‚ +β”‚ FILESYSTEM (host) β”‚ +β”‚ β”‚ +β”‚ /data/jobs/{job_id}/ β”‚ +β”‚ β”œβ”€β”€ input/ ← client coloca arquivos aqui β”‚ +β”‚ β”‚ β”œβ”€β”€ vendas.xlsx (2GB) worker lΓͺ via bind mount β”‚ +β”‚ β”‚ └── config.json β”‚ +β”‚ └── output/ ← worker escreve aqui β”‚ +β”‚ └── resultado.parquet client lΓͺ direto β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + + + + FLUXO DE EXECUÇÃO +──────────────────────────────────────────────────────────────────────────────── + + TEMPO CLIENT FFI DAEMON WORKER +─────────────────────────────────────────────────────────────────────────────── + + 0ms β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Prepara job β”‚ + β”‚ em /data/ β”‚ + β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + 1ms β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + └────────►│ serialize β”‚ + β”‚ request β”‚ + β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + 2ms β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + └────────►│ recv socket β”‚ + β”‚ parse msg β”‚ + β”‚ validate β”‚ + β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + 3ms β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + └────────►│ recv pipe β”‚ + β”‚ setup mountsβ”‚ + β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + 4ms β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ β”‚ + β”‚ SANDBOX EXECUTION β”‚ + β”‚ β”‚ + β”‚ /input/vendas.xlsx ◄─── bind mount ◄─── /data/jobs/abc/input/ β”‚ + β”‚ β”‚ β”‚ + β”‚ β–Ό β”‚ + β”‚ df = pd.read_excel('/input/vendas.xlsx') # zero-copy read β”‚ + β”‚ summary = df.groupby('region').sum() β”‚ + β”‚ summary.to_parquet('/output/result.parquet') β”‚ + β”‚ β”‚ β”‚ + β”‚ β–Ό β”‚ + β”‚ /output/result.parquet ──► bind mount ──► /data/jobs/abc/output/ β”‚ + β”‚ β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”‚ (tempo de execuΓ§Γ£o variΓ‘vel) + β”‚ + Nms β”‚ + └────────────────────────────────────────┐ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ collect β”‚ + β”‚ stdout/err β”‚ + β”‚ metrics β”‚ + β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”‚β—„β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β” + β”‚ serialize β”‚ + β”‚ response β”‚ + β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”‚β—„β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β” + β”‚ return β”‚ + β”‚ result β”‚ + β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ LΓͺ output β”‚ + β”‚ de /data/ β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + + + + PROTOCOLO +──────────────────────────────────────────────────────────────────────────────── + +REQUEST (Client β†’ Daemon) +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ 4 bytes β”‚ msgpack payload β”‚ β”‚ +β”‚ β”‚ (length) β”‚ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ { β”‚ +β”‚ "type": "Execute", β”‚ +β”‚ "code": "df = pd.read_excel('/input/data.xlsx')...", β”‚ +β”‚ "mounts": [ β”‚ +β”‚ {"host": "/data/jobs/abc/input", "sandbox": "/input", "ro": true}, β”‚ +β”‚ {"host": "/data/jobs/abc/output", "sandbox": "/output", "ro": false} β”‚ +β”‚ ], β”‚ +β”‚ "timeout_secs": 300, β”‚ +β”‚ "memory_limit": 4294967296 β”‚ +β”‚ } β”‚ +β”‚ β”‚ +β”‚ ~200 bytes β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + + +RESPONSE (Daemon β†’ Client) +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ 4 bytes β”‚ msgpack payload β”‚ β”‚ +β”‚ β”‚ (length) β”‚ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ { β”‚ +β”‚ "type": "Execute", β”‚ +β”‚ "success": true, β”‚ +β”‚ "result": { β”‚ +β”‚ "exit_code": 0, β”‚ +β”‚ "stdout": "Processed 1000000 rows", β”‚ +β”‚ "stderr": "", β”‚ +β”‚ "duration_ms": 45230, β”‚ +β”‚ "memory_peak": 3221225472, β”‚ +β”‚ "cpu_time_us": 44890000, β”‚ +β”‚ "timed_out": false, β”‚ +β”‚ "oom_killed": false β”‚ +β”‚ } β”‚ +β”‚ } β”‚ +β”‚ β”‚ +β”‚ ~500 bytes β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + + + + ISOLAMENTO (WORKER) +──────────────────────────────────────────────────────────────────────────────── + +ORDEM DE APLICAÇÃO (crΓ­tico para seguranΓ§a): + + 1. CLONE/UNSHARE + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ clone3(CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNS | β”‚ + β”‚ CLONE_NEWNET | CLONE_NEWIPC | CLONE_NEWUTS) β”‚ + β”‚ β”‚ + β”‚ β†’ Processo em novos namespaces β”‚ + β”‚ β†’ UID 0 dentro = UID 1000 fora (mapeado) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + 2. FILESYSTEM SETUP + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ mount("none", "/", NULL, MS_REC | MS_PRIVATE, NULL) β”‚ + β”‚ β”‚ + β”‚ // Bind mounts β”‚ + β”‚ mount("/data/jobs/abc/input", "/sandbox/input", NULL, MS_BIND, NULL)β”‚ + β”‚ mount(NULL, "/sandbox/input", NULL, MS_REMOUNT | MS_RDONLY, NULL) β”‚ + β”‚ mount("/data/jobs/abc/output", "/sandbox/output", NULL, MS_BIND, 0) β”‚ + β”‚ β”‚ + β”‚ // Sistema read-only β”‚ + β”‚ mount("/usr", "/sandbox/usr", NULL, MS_BIND | MS_RDONLY, NULL) β”‚ + β”‚ mount("/lib", "/sandbox/lib", NULL, MS_BIND | MS_RDONLY, NULL) β”‚ + β”‚ β”‚ + β”‚ // tmpfs para temp β”‚ + β”‚ mount("tmpfs", "/sandbox/tmp", "tmpfs", 0, "size=100M") β”‚ + β”‚ β”‚ + β”‚ // pivot_root β”‚ + β”‚ pivot_root("/sandbox", "/sandbox/.old") β”‚ + β”‚ umount2("/.old", MNT_DETACH) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + 3. LANDLOCK + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ ruleset_fd = landlock_create_ruleset(&attr, size, 0) β”‚ + β”‚ β”‚ + β”‚ // Permite leitura β”‚ + β”‚ landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, β”‚ + β”‚ {.allowed_access = READ, .parent_fd = /input}) β”‚ + β”‚ landlock_add_rule(..., {READ, /usr}) β”‚ + β”‚ landlock_add_rule(..., {READ, /lib}) β”‚ + β”‚ β”‚ + β”‚ // Permite escrita β”‚ + β”‚ landlock_add_rule(..., {READ | WRITE, /output}) β”‚ + β”‚ landlock_add_rule(..., {READ | WRITE, /tmp}) β”‚ + β”‚ β”‚ + β”‚ landlock_restrict_self(ruleset_fd, 0) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + 4. CAPABILITIES DROP + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ cap_clear(caps) β”‚ + β”‚ cap_set_proc(caps) β”‚ + β”‚ β”‚ + β”‚ prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + 5. SECCOMP (ΓΊltimo - trava interface de syscalls) + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ BPF filter (whitelist): β”‚ + β”‚ β”‚ + β”‚ ALLOW: read, write, close, fstat, lseek, mmap, mprotect, β”‚ + β”‚ munmap, brk, rt_sigaction, rt_sigprocmask, ioctl, β”‚ + β”‚ access, dup, dup2, getpid, getuid, getgid, geteuid, β”‚ + β”‚ getegid, fcntl, openat, newfstatat, exit, exit_group, β”‚ + β”‚ futex, getrandom, clock_gettime, clock_nanosleep β”‚ + β”‚ β”‚ + β”‚ DENY (with EACCES via notify): everything else β”‚ + β”‚ β”‚ + β”‚ seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, β”‚ + β”‚ &prog) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + 6. CGROUPS + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ // Daemon jΓ‘ criou cgroup antes do fork β”‚ + β”‚ /sys/fs/cgroup/leeward/worker-{id}/ β”‚ + β”‚ β”‚ + β”‚ memory.max = 4294967296 (4GB) β”‚ + β”‚ memory.swap.max = 0 (no swap) β”‚ + β”‚ cpu.max = 100000 100000 (100%) β”‚ + β”‚ pids.max = 64 (max processes) β”‚ + β”‚ β”‚ + β”‚ // Worker adicionado ao cgroup β”‚ + β”‚ echo $PID > /sys/fs/cgroup/leeward/worker-{id}/cgroup.procs β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + 7. EXEC PYTHON + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ execve("/usr/bin/python3", ["python3", "-c", code], env) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + + + + WORKER POOL LIFECYCLE +──────────────────────────────────────────────────────────────────────────────── + +STARTUP +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”‚ +β”‚ Daemon β”‚ +β”‚ β”‚ β”‚ +β”‚ β”œβ”€β”€β–Ί fork() ──► Worker 1 ──► setup isolation ──► Python ready (idle) β”‚ +β”‚ β”œβ”€β”€β–Ί fork() ──► Worker 2 ──► setup isolation ──► Python ready (idle) β”‚ +β”‚ β”œβ”€β”€β–Ί fork() ──► Worker 3 ──► setup isolation ──► Python ready (idle) β”‚ +β”‚ └──► fork() ──► Worker 4 ──► setup isolation ──► Python ready (idle) β”‚ +β”‚ β”‚ +β”‚ Pool: [W1:idle] [W2:idle] [W3:idle] [W4:idle] β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + + +REQUEST HANDLING +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”‚ +β”‚ Request chega β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Pool: [W1:idle] [W2:idle] [W3:idle] [W4:idle] β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ └──► grab W1 β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ Pool: [W1:busy] [W2:idle] [W3:idle] [W4:idle] β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β”œβ”€β”€β–Ί send code via pipe β”‚ +β”‚ β”‚ β”‚ +β”‚ β”‚ W1 executes... β”‚ +β”‚ β”‚ β”‚ +β”‚ ◄──► recv result via pipe β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ W1.execution_count++ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ if execution_count >= 100: β”‚ β”‚ +β”‚ β”‚ recycle(W1) # kill + respawn β”‚ β”‚ +β”‚ β”‚ else: β”‚ β”‚ +β”‚ β”‚ W1.state = idle β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ Pool: [W1:idle] [W2:idle] [W3:idle] [W4:idle] β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + + +RECYCLING +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”‚ +β”‚ Por que reciclar: β”‚ +β”‚ - Memory leaks no Python β”‚ +β”‚ - Estado poluΓ­do entre execuΓ§Γ΅es β”‚ +β”‚ - SeguranΓ§a (limpa qualquer state residual) β”‚ +β”‚ β”‚ +β”‚ recycle(W1): β”‚ +β”‚ β”‚ β”‚ +β”‚ β”œβ”€β”€β–Ί W1.state = recycling β”‚ +β”‚ β”œβ”€β”€β–Ί kill(W1.pid, SIGKILL) β”‚ +β”‚ β”œβ”€β”€β–Ί waitpid(W1.pid) β”‚ +β”‚ β”œβ”€β”€β–Ί destroy_cgroup(W1) β”‚ +β”‚ β”œβ”€β”€β–Ί fork() ──► new W1 β”‚ +β”‚ β”œβ”€β”€β–Ί setup isolation β”‚ +β”‚ └──► W1.state = idle, W1.execution_count = 0 β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + + + + ESTRUTURA +──────────────────────────────────────────────────────────────────────────────── + +CRATES +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”‚ +β”‚ leeward-core β”‚ +β”‚ β”œβ”€β”€ src/ β”‚ +β”‚ β”‚ β”œβ”€β”€ lib.rs β”‚ +β”‚ β”‚ β”œβ”€β”€ config.rs # SandboxConfig β”‚ +β”‚ β”‚ β”œβ”€β”€ error.rs # LeewardError β”‚ +β”‚ β”‚ β”œβ”€β”€ result.rs # ExecutionResult β”‚ +β”‚ β”‚ β”œβ”€β”€ worker.rs # Worker struct β”‚ +β”‚ β”‚ └── isolation/ β”‚ +β”‚ β”‚ β”œβ”€β”€ mod.rs β”‚ +β”‚ β”‚ β”œβ”€β”€ namespace.rs # clone/unshare β”‚ +β”‚ β”‚ β”œβ”€β”€ mounts.rs # bind mounts, pivot_root β”‚ +β”‚ β”‚ β”œβ”€β”€ landlock.rs # filesystem restrictions β”‚ +β”‚ β”‚ β”œβ”€β”€ seccomp.rs # syscall filter β”‚ +β”‚ β”‚ └── cgroups.rs # resource limits β”‚ +β”‚ β”‚ β”‚ +β”‚ leeward-daemon β”‚ +β”‚ β”œβ”€β”€ src/ β”‚ +β”‚ β”‚ β”œβ”€β”€ main.rs # tokio server β”‚ +β”‚ β”‚ β”œβ”€β”€ config.rs # DaemonConfig β”‚ +β”‚ β”‚ β”œβ”€β”€ server.rs # socket handler β”‚ +β”‚ β”‚ β”œβ”€β”€ pool.rs # WorkerPool β”‚ +β”‚ β”‚ └── protocol.rs # msgpack Request/Response β”‚ +β”‚ β”‚ β”‚ +β”‚ leeward-ffi β”‚ +β”‚ β”œβ”€β”€ src/lib.rs # C FFI exports β”‚ +β”‚ β”œβ”€β”€ build.rs # cbindgen β”‚ +β”‚ └── cbindgen.toml β”‚ +β”‚ β”‚ β”‚ +β”‚ leeward-cli β”‚ +β”‚ └── src/main.rs # CLI tool β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + + +BINDINGS +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”‚ +β”‚ bindings/ β”‚ +β”‚ β”œβ”€β”€ python/ β”‚ +β”‚ β”‚ β”œβ”€β”€ leeward/ β”‚ +β”‚ β”‚ β”‚ β”œβ”€β”€ __init__.py β”‚ +β”‚ β”‚ β”‚ └── client.py # ctypes wrapper β”‚ +β”‚ β”‚ └── pyproject.toml β”‚ +β”‚ β”‚ β”‚ +β”‚ β”œβ”€β”€ node/ (futuro) β”‚ +β”‚ β”‚ └── ... # ffi-napi β”‚ +β”‚ β”‚ β”‚ +β”‚ └── go/ (futuro) β”‚ +β”‚ └── ... # cgo β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + + + + MΓ‰TRICAS +──────────────────────────────────────────────────────────────────────────────── + +PROMETHEUS (porta 9090) +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”‚ +β”‚ # Pool β”‚ +β”‚ leeward_workers_total{state="idle|busy|recycling"} β”‚ +β”‚ leeward_worker_recycles_total β”‚ +β”‚ β”‚ +β”‚ # ExecuΓ§Γ΅es β”‚ +β”‚ leeward_executions_total β”‚ +β”‚ leeward_executions_failed_total{reason="timeout|oom|error"} β”‚ +β”‚ leeward_execution_duration_seconds{quantile="0.5|0.9|0.99"} β”‚ +β”‚ β”‚ +β”‚ # Recursos β”‚ +β”‚ leeward_memory_usage_bytes{worker="1|2|3|4"} β”‚ +β”‚ leeward_cpu_time_seconds_total β”‚ +β”‚ β”‚ +β”‚ # Socket β”‚ +β”‚ leeward_connections_active β”‚ +β”‚ leeward_requests_total β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + + + + PERFORMANCE +──────────────────────────────────────────────────────────────────────────────── + +LATÊNCIA BREAKDOWN (warm request, print('hello')) +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”‚ +β”‚ Client serialize 0.05ms β”‚ +β”‚ Socket send 0.10ms β”‚ +β”‚ Daemon recv + parse 0.10ms β”‚ +β”‚ Pool get worker 0.05ms β”‚ +β”‚ Pipe send to worker 0.10ms β”‚ +β”‚ Worker recv 0.05ms β”‚ +β”‚ Mount setup (per-job) 0.50ms β”‚ +β”‚ Python exec 1.50ms β”‚ +β”‚ Collect stdout 0.10ms β”‚ +β”‚ Pipe send result 0.10ms β”‚ +β”‚ Daemon serialize 0.05ms β”‚ +β”‚ Socket send 0.10ms β”‚ +β”‚ Client deserialize 0.05ms β”‚ +β”‚ ───────────────────────────────── β”‚ +β”‚ TOTAL ~3ms β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + + +THROUGHPUT +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”‚ +β”‚ 4 workers Γ— 300 req/s/worker = ~1,200 req/s β”‚ +β”‚ 8 workers Γ— 300 req/s/worker = ~2,400 req/s β”‚ +β”‚ 16 workers Γ— 300 req/s/worker = ~4,800 req/s β”‚ +β”‚ β”‚ +β”‚ Bottleneck: Python execution time β”‚ +β”‚ Com pandas hot: ~50 req/s/worker (processamento real) β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + + + + COMPARAÇÃO FINAL +──────────────────────────────────────────────────────────────────────────────── + +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”‚ E2B β”‚ Docker β”‚ Modal β”‚ WASM β”‚ leeward β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Cold start β”‚ 200ms β”‚ 400ms β”‚ 150ms β”‚ 5ms β”‚ 45ms β”‚ +β”‚ Warm request β”‚ 50ms β”‚ 80ms β”‚ 30ms β”‚ 1ms β”‚ 3ms β”‚ +β”‚ 2GB file β”‚ 10s+ β”‚ 50ms β”‚ 10s+ β”‚ N/A β”‚ ~0ms (bind mount) β”‚ +β”‚ Throughput β”‚ 500/s β”‚ 100/s β”‚ 1000/s β”‚ 5000/s β”‚ 5000/s β”‚ +β”‚ Mem/worker β”‚ 50MB β”‚ 100MB β”‚ 50MB β”‚ 10MB β”‚ 15MB β”‚ +β”‚ Native libs β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ— β”‚ βœ“ β”‚ +β”‚ Self-hosted β”‚ βœ— β”‚ βœ“ β”‚ βœ— β”‚ βœ“ β”‚ βœ“ β”‚ +β”‚ Pricing β”‚ $$$ β”‚ server β”‚ $$ β”‚ free β”‚ free β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ \ No newline at end of file diff --git a/nix/module.nix b/nix/module.nix index 1df18fc..6041ec3 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -5,17 +5,24 @@ with lib; let cfg = config.services.leeward; - leeward = pkgs.callPackage ./packages.nix { inherit pkgs; }; + leewardLib = import ./lib.nix { inherit pkgs; }; + leewardPkgs = import ./packages.nix { inherit pkgs; lib = leewardLib; }; in { options.services.leeward = { enable = mkEnableOption "leeward sandbox daemon"; package = mkOption { type = types.package; - default = leeward.leeward-daemon; + default = leewardPkgs.leeward-daemon; description = "The leeward package to use."; }; + socketPath = mkOption { + type = types.str; + default = "/run/leeward/leeward.sock"; + description = "Path to the Unix socket."; + }; + numWorkers = mkOption { type = types.int; default = 4; @@ -27,32 +34,94 @@ in { default = 100; description = "Recycle workers after this many executions."; }; + + memoryLimit = mkOption { + type = types.str; + default = "256M"; + description = "Memory limit per worker (e.g., 256M, 1G)."; + }; + + cpuQuota = mkOption { + type = types.str; + default = "100%"; + description = "CPU quota for workers (e.g., 100%, 200% for 2 cores)."; + }; + + user = mkOption { + type = types.str; + default = "leeward"; + description = "User to run the daemon as."; + }; + + group = mkOption { + type = types.str; + default = "leeward"; + description = "Group for socket access."; + }; }; config = mkIf cfg.enable { + # Kernel parameters for sandbox features + boot.kernel.sysctl = { + "kernel.unprivileged_userns_clone" = 1; + }; + systemd.services.leeward = { description = "Leeward sandbox daemon"; documentation = [ "https://github.com/vektia/leeward" ]; after = [ "network.target" ]; wantedBy = [ "multi-user.target" ]; + environment = { + LEEWARD_SOCKET = cfg.socketPath; + LEEWARD_WORKERS = toString cfg.numWorkers; + LEEWARD_RECYCLE_AFTER = toString cfg.recycleAfter; + }; + serviceConfig = { Type = "simple"; ExecStart = "${cfg.package}/bin/leeward-daemon"; Restart = "on-failure"; RestartSec = "5s"; + # Run as dedicated user + User = cfg.user; + Group = cfg.group; + + # Cgroup delegation - CRITICAL for sandbox to create worker cgroups + Delegate = "cpu cpuset io memory pids"; + # Security hardening - NoNewPrivileges = true; + NoNewPrivileges = false; # Need for user namespaces PrivateTmp = true; ProtectSystem = "strict"; ProtectHome = true; ReadWritePaths = [ "/run/leeward" ]; + # Capabilities needed for namespaces and cgroups + AmbientCapabilities = [ + "CAP_SYS_ADMIN" # For mount namespaces + "CAP_SETUID" # For user namespaces + "CAP_SETGID" # For user namespaces + "CAP_NET_ADMIN" # For network namespaces + "CAP_SYS_PTRACE" # For seccomp user notifications + ]; + CapabilityBoundingSet = [ + "CAP_SYS_ADMIN" + "CAP_SETUID" + "CAP_SETGID" + "CAP_NET_ADMIN" + "CAP_SYS_PTRACE" + ]; + # Runtime directory RuntimeDirectory = "leeward"; RuntimeDirectoryMode = "0755"; + # Resource limits for the daemon itself + MemoryMax = "2G"; + TasksMax = "256"; + # Logging StandardOutput = "journal"; StandardError = "journal"; @@ -60,12 +129,21 @@ in { }; }; - # Create group for socket access - users.groups.leeward = {}; + # Create dedicated user and group + users.users.${cfg.user} = { + isSystemUser = true; + group = cfg.group; + description = "Leeward sandbox daemon user"; + }; + + users.groups.${cfg.group} = {}; # Ensure runtime directory permissions systemd.tmpfiles.rules = [ - "d /run/leeward 0755 root leeward - -" + "d /run/leeward 0755 ${cfg.user} ${cfg.group} - -" ]; + + # Add CLI to system packages + environment.systemPackages = [ leewardPkgs.leeward-cli ]; }; } diff --git a/nix/packages.nix b/nix/packages.nix index faa7c79..62c4077 100644 --- a/nix/packages.nix +++ b/nix/packages.nix @@ -5,6 +5,21 @@ let buildDeps = lib.buildDeps; src = pkgs.lib.cleanSource ../.; + # Python with commonly needed packages for sandboxed execution + pythonEnv = pkgs.python3.withPackages (ps: with ps; [ + # Base packages users might expect + ]); + + # Runtime dependencies that need to be available in the sandbox + runtimeDeps = [ + pythonEnv + pkgs.coreutils + pkgs.bash + ]; + + # Paths to bind-mount into sandbox (read-only) + sandboxPaths = pkgs.lib.concatMapStringsSep ":" (p: "${p}") runtimeDeps; + rustBuild = { inherit version src; cargoLock.lockFile = ../Cargo.lock; @@ -13,18 +28,80 @@ let RUSTFLAGS = "-C link-arg=-fuse-ld=mold"; }; - leeward-cli = pkgs.rustPlatform.buildRustPackage (rustBuild // { + leeward-cli-unwrapped = pkgs.rustPlatform.buildRustPackage (rustBuild // { pname = "leeward-cli"; cargoBuildFlags = [ "-p" "leeward-cli" ]; cargoTestFlags = [ "-p" "leeward-cli" ]; }); - leeward-daemon = pkgs.rustPlatform.buildRustPackage (rustBuild // { + leeward-daemon-unwrapped = pkgs.rustPlatform.buildRustPackage (rustBuild // { pname = "leeward-daemon"; cargoBuildFlags = [ "-p" "leeward-daemon" ]; cargoTestFlags = [ "-p" "leeward-daemon" ]; }); + # Wrapped daemon with all runtime dependencies in PATH + leeward-daemon = pkgs.stdenv.mkDerivation { + pname = "leeward-daemon"; + inherit version; + + nativeBuildInputs = [ pkgs.makeWrapper ]; + + # No source, just wrapping + dontUnpack = true; + + installPhase = '' + runHook preInstall + + mkdir -p $out/bin $out/share/leeward + + # Create wrapper with proper environment + makeWrapper ${leeward-daemon-unwrapped}/bin/leeward-daemon $out/bin/leeward-daemon \ + --prefix PATH : "${pkgs.lib.makeBinPath runtimeDeps}" \ + --set LEEWARD_PYTHON_PATH "${pythonEnv}/bin/python3" \ + --set LEEWARD_SANDBOX_PATHS "${sandboxPaths}" + + # Symlink the unwrapped binary for debugging + ln -s ${leeward-daemon-unwrapped}/bin/leeward-daemon $out/bin/leeward-daemon-unwrapped + + runHook postInstall + ''; + + meta = with pkgs.lib; { + description = "Leeward sandbox daemon with pre-configured Python environment"; + homepage = "https://github.com/vektia/leeward"; + license = licenses.asl20; + platforms = platforms.linux; + }; + }; + + # Wrapped CLI + leeward-cli = pkgs.stdenv.mkDerivation { + pname = "leeward-cli"; + inherit version; + + nativeBuildInputs = [ pkgs.makeWrapper ]; + dontUnpack = true; + + installPhase = '' + runHook preInstall + + mkdir -p $out/bin + + makeWrapper ${leeward-cli-unwrapped}/bin/leeward $out/bin/leeward \ + --prefix PATH : "${pkgs.lib.makeBinPath runtimeDeps}" + + runHook postInstall + ''; + + meta = with pkgs.lib; { + description = "Leeward CLI"; + homepage = "https://github.com/vektia/leeward"; + license = licenses.asl20; + platforms = platforms.linux; + }; + }; + leeward-ffi = pkgs.rustPlatform.buildRustPackage (rustBuild // { pname = "leeward-ffi"; cargoBuildFlags = [ "-p" "leeward-ffi" ]; @@ -44,9 +121,17 @@ let in { inherit leeward-cli leeward-daemon leeward-ffi; + inherit leeward-cli-unwrapped leeward-daemon-unwrapped; + inherit pythonEnv runtimeDeps; leeward-all = pkgs.symlinkJoin { name = "leeward-${version}"; paths = [ leeward-cli leeward-daemon leeward-ffi ]; + meta = with pkgs.lib; { + description = "Complete leeward sandbox suite"; + homepage = "https://github.com/vektia/leeward"; + license = licenses.asl20; + platforms = platforms.linux; + }; }; } \ No newline at end of file diff --git a/nix/shell.nix b/nix/shell.nix deleted file mode 100644 index 5254adc..0000000 --- a/nix/shell.nix +++ /dev/null @@ -1,19 +0,0 @@ -{ pkgs, config, ... }: - -{ - packages = with pkgs; [ - cargo-watch - pkg-config - libseccomp - mold - ]; - - languages.rust.enable = true; - - env = { - LIBSECCOMP_LINK_TYPE = "dylib"; - LIBSECCOMP_LIB_PATH = "${pkgs.libseccomp}/lib"; - PKG_CONFIG_PATH = "${pkgs.libseccomp}/lib/pkgconfig"; - LEEWARD_SOCKET = "${config.env.DEVENV_STATE}/leeward.sock"; - }; -} \ No newline at end of file From ca65fb6250e35afefd711007438c738e390c6eb8 Mon Sep 17 00:00:00 2001 From: fullzer4 Date: Fri, 2 Jan 2026 13:01:10 -0300 Subject: [PATCH 2/3] fix: include Cargo.lock for nix builds --- .gitignore | 1 - Cargo.lock | 1030 ++++++++++++++++++++++++++++++++++++++++++++++++++++ flake.lock | 12 +- 3 files changed, 1036 insertions(+), 7 deletions(-) create mode 100644 Cargo.lock diff --git a/.gitignore b/.gitignore index e07339f..45f4448 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,6 @@ build/ *.egg-info/ target/ -Cargo.lock *.whl *.so diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..c13eec1 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1030 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "caps" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd1ddba47aba30b6a889298ad0109c3b8dcb0e8fc993b459daa7067d46f865e0" +dependencies = [ + "libc", +] + +[[package]] +name = "cbindgen" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadd868a2ce9ca38de7eeafdcec9c7065ef89b42b32f0839278d55f35c54d1ff" +dependencies = [ + "clap", + "heck 0.4.1", + "indexmap", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn", + "tempfile", + "toml", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "clap" +version = "4.5.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "io-uring" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd7bddefd0a8833b88a4b68f90dae22c7450d11b354198baee3874fd811b344" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "landlock" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49fefd6652c57d68aaa32544a4c0e642929725bdc1fd929367cdeb673ab81088" +dependencies = [ + "enumflags2", + "libc", + "thiserror", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leeward-cli" +version = "0.0.1" +dependencies = [ + "clap", + "leeward-core", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "leeward-core" +version = "0.0.1" +dependencies = [ + "caps", + "landlock", + "libc", + "memfd", + "nix", + "rmp-serde", + "seccompiler", + "serde", + "thiserror", + "tracing", +] + +[[package]] +name = "leeward-daemon" +version = "0.0.1" +dependencies = [ + "anyhow", + "io-uring", + "leeward-core", + "memfd", + "parking_lot", + "rmp-serde", + "serde", + "serde_json", + "signal-hook", + "signal-hook-tokio", + "thiserror", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "leeward-ffi" +version = "0.0.1" +dependencies = [ + "cbindgen", + "leeward-core", + "libc", + "once_cell", + "thiserror", +] + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "proc-macro2" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rmp" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "rmp-serde" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" +dependencies = [ + "rmp", + "serde", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "seccompiler" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "345a3e4dddf721a478089d4697b83c6c0a8f5bf16086f6c13397e4534eb6e2e5" +dependencies = [ + "libc", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.148" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signal-hook-tokio" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213241f76fb1e37e27de3b6aa1b068a2c333233b59cca6634f634b80a27ecf1e" +dependencies = [ + "futures-core", + "libc", + "signal-hook", + "tokio", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "zmij" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d6085d62852e35540689d1f97ad663e3971fc19cf5eceab364d62c646ea167" diff --git a/flake.lock b/flake.lock index 1f011b4..bafa343 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1761114652, - "narHash": "sha256-f/QCJM/YhrV/lavyCVz8iU3rlZun6d+dAiC3H+CDle4=", + "lastModified": 1767116409, + "narHash": "sha256-5vKw92l1GyTnjoLzEagJy5V5mDFck72LiQWZSOnSicw=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "01f116e4df6a15f4ccdffb1bcd41096869fb385c", + "rev": "cad22e7d996aea55ecab064e84834289143e44a0", "type": "github" }, "original": { @@ -62,11 +62,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1766890375, - "narHash": "sha256-0Zi7ChAtjq/efwQYmp7kOJPcSt6ya9ynSUe6ppgZhsQ=", + "lastModified": 1767322002, + "narHash": "sha256-yHKXXw2OWfIFsyTjduB4EyFwR0SYYF0hK8xI9z4NIn0=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "91e1f7a0017065360f447622d11b7ce6ed04772f", + "rev": "03c6e38661c02a27ca006a284813afdc461e9f7e", "type": "github" }, "original": { From 1aa3dc62230438aad8bb7e401c9b2f70f69cc907 Mon Sep 17 00:00:00 2001 From: fullzer4 Date: Fri, 2 Jan 2026 15:35:58 -0300 Subject: [PATCH 3/3] refactor: remove cgroups and simplify v1 - Remove cgroups support for simpler v1 - Consolidate Nix config into single flake.nix - Centralize versioning in Cargo.toml - Add release automation with GitHub Actions - Clean up unnecessary files and directories - Update docs to English (CONTRIBUTING.md, INSTALL.md) - Simplify README to minimal example - Update license references to Apache-2.0 --- .github/workflows/ci.yml | 37 ++ .github/workflows/release.yml | 112 ++++ .gitignore | 17 +- ADOPTION.md | 24 - CHANGELOG.md | 39 ++ CLAUDE.md | 265 --------- CONTRIBUTING.md | 277 ++++++++++ Cargo.toml | 4 +- INSTALL.md | 325 +++++++++++ README.md | 89 +-- contrib/leeward.system.service | 26 + contrib/leeward.user.service | 14 + crates/leeward-cli/src/main.rs | 17 +- crates/leeward-core/src/config.rs | 29 - crates/leeward-core/src/error.rs | 3 - crates/leeward-core/src/isolation/cgroups.rs | 241 -------- crates/leeward-core/src/isolation/clone3.rs | 21 +- crates/leeward-core/src/isolation/mod.rs | 5 +- crates/leeward-core/src/lib.rs | 1 - crates/leeward-core/src/worker.rs | 38 +- docs/install.md | 278 ---------- flake.lock | 20 +- flake.nix | 267 +++++++-- ideia.md | 549 ------------------- nix/lib.nix | 14 - nix/module.nix | 149 ----- nix/packages.nix | 137 ----- 27 files changed, 1091 insertions(+), 1907 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml delete mode 100644 ADOPTION.md create mode 100644 CHANGELOG.md delete mode 100644 CLAUDE.md create mode 100644 CONTRIBUTING.md create mode 100644 INSTALL.md create mode 100644 contrib/leeward.system.service create mode 100644 contrib/leeward.user.service delete mode 100644 crates/leeward-core/src/isolation/cgroups.rs delete mode 100644 docs/install.md delete mode 100644 ideia.md delete mode 100644 nix/lib.nix delete mode 100644 nix/module.nix delete mode 100644 nix/packages.nix diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..03197b2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Nix + uses: cachix/install-nix-action@v24 + with: + extra_nix_config: | + experimental-features = nix-command flakes + + - name: Check flake + run: nix flake check + + - name: Format check + run: nix develop -c cargo fmt -- --check + + - name: Clippy + run: nix develop -c cargo clippy --all-targets --all-features -- -D warnings + + - name: Test + run: nix develop -c cargo test --workspace + + - name: Build + run: nix build .#leeward-x86_64 + + - name: Security audit + run: nix develop -c cargo audit \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d3d8391 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,112 @@ +name: Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + tag: + description: 'Release tag (e.g., v0.1.0)' + required: true + type: string + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Nix + uses: cachix/install-nix-action@v24 + with: + extra_nix_config: | + experimental-features = nix-command flakes + accept-flake-config = true + + - name: Setup Cachix + uses: cachix/cachix-action@v14 + with: + name: leeward + skipPush: true + + - name: Build all packages + run: | + # Build all outputs + nix build .#leeward-x86_64 -L + nix build .#leeward-aarch64 -L + nix build .#leeward-static -L + nix build .#leeward-deb -L + + # Copy artifacts + mkdir -p artifacts + cp -L result*/* artifacts/ || true + + - name: Create release archives + run: | + cd artifacts + + # Create tarballs for each architecture + for arch in x86_64 aarch64; do + if [ -d "leeward-$arch" ]; then + tar czf "leeward-${arch}-linux.tar.gz" "leeward-$arch" + fi + done + + # Create checksums + sha256sum *.tar.gz *.deb > checksums.txt || true + + cd .. + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: release-artifacts + path: artifacts/* + + release: + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: release-artifacts + path: artifacts + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ github.event.inputs.tag || github.ref_name }} + name: Release ${{ github.event.inputs.tag || github.ref_name }} + draft: false + prerelease: false + files: | + artifacts/*.tar.gz + artifacts/*.deb + artifacts/checksums.txt + body: | + ## Installation + + ### Debian/Ubuntu + ```bash + wget https://github.com/vektia/leeward/releases/latest/download/leeward_*_amd64.deb + sudo dpkg -i leeward_*.deb + ``` + + ### Binary (x86_64) + ```bash + curl -L https://github.com/vektia/leeward/releases/latest/download/leeward-x86_64-linux.tar.gz | tar xz + sudo mv leeward-x86_64/* /usr/local/bin/ + ``` + + ### Nix + ```nix + nix run github:vektia/leeward/${{ github.event.inputs.tag || github.ref_name }} + ``` + + See [CHANGELOG.md](https://github.com/vektia/leeward/blob/main/CHANGELOG.md) for changes. \ No newline at end of file diff --git a/.gitignore b/.gitignore index 45f4448..d10f70d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +# Python __pycache__/ .pytest_cache/ .ruff_cache @@ -9,18 +10,22 @@ __pycache__/ dist/ build/ *.egg-info/ +*.whl +# Rust target/ - -*.whl *.so +# Development tools .direnv/ .devenv/ .pre-commit-config.yaml - -/target - .cargo-home/ -include/ \ No newline at end of file +# Nix build results +result +result-* + +# Runtime files +*.sock +.leeward.sock \ No newline at end of file diff --git a/ADOPTION.md b/ADOPTION.md deleted file mode 100644 index 7a1d47e..0000000 --- a/ADOPTION.md +++ /dev/null @@ -1,24 +0,0 @@ -# Using leeward? - -We'd love to hear from you. Knowing who uses leeward helps us: - -- πŸ“Š Demonstrate impact to sponsors -- 🎯 Prioritize features that matter -- πŸ’ͺ Keep the project alive and maintained - -## How to let us know - -**Option 1: Public** - -Open an issue with the `adoption` label including: -- Company name (or "confidential") -- Use case (one line) -- Approximate executions/month - -**Option 2: Private** - -Email hello@vektia.com.br - ---- - -Thank you for strengthening open source πŸ™ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d49d528 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,39 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +- Initial release of leeward +- Pre-fork worker pool architecture for ~0.5ms latency +- Linux namespace isolation (pid, mount, net, ipc, uts) +- Seccomp syscall filtering +- Landlock filesystem restrictions +- Unix socket IPC between daemon and CLI +- MessagePack protocol for communication +- Systemd service files (system and user) +- Nix flake support with overlay +- Debian package generation +- Static musl binary builds +- Multi-architecture support (x86_64, aarch64) + +### Architecture +- `leeward-core`: Core isolation primitives +- `leeward-daemon`: Persistent daemon with worker pool +- `leeward-cli`: Command-line interface +- `leeward-ffi`: C FFI library for language bindings + +### Security Features +- No root required (uses user namespaces) +- Defense in depth with 3 isolation layers +- Zero network access by default +- Restricted filesystem access via Landlock +- Minimal syscall whitelist via seccomp + +## [0.1.0] - TBD + +Initial public release. \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index d2f426e..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,265 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -**leeward** is a Linux-native sandbox for running untrusted code (currently Python) with extremely low latency (~0.5ms vs Docker's 300-500ms). It's designed for AI agent code execution using native Linux primitives instead of containers or VMs. - -**Status:** Work in progress. Implementing advanced pre-fork pool with io_uring and shared memory. - -## Architecture - -### Pre-Fork Execution Model -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” io_uring/shm β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Client β”‚ ◄───────────────► β”‚ leeward daemon β”‚ -β”‚ (any lang) β”‚ zero-copy IPC β”‚ β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ - β”‚ β”‚ Pre-warmed Worker Pool β”‚ β”‚ - Python, Go, β”‚ β”‚ β”‚ β”‚ - Node, Rust β”‚ β”‚ [W1] Python idle ──pipe β”‚ β”‚ - via C FFI β”‚ β”‚ [W2] Python idle ──pipe β”‚ β”‚ - β”‚ β”‚ [W3] Python idle ──pipe β”‚ β”‚ - β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` - -- **leeward-daemon**: Persistent daemon maintaining pre-forked worker pool -- **leeward-cli**: Command-line interface for executing code -- **leeward-ffi**: C FFI library for multi-language support -- **leeward-core**: Core isolation primitives and worker management - -### Performance Architecture (4 Levels) - -**Level 1: Pre-fork Pool (~0.5ms)** -- Workers created at daemon startup using `clone3` with `CLONE_INTO_CGROUP` -- Python interpreter already loaded and idle -- Code sent via pipe, execution happens immediately -- No fork/exec overhead on each request - -**Level 2: io_uring IPC (~0.2ms savings)** -- Zero-copy async I/O using io_uring submission queues -- 1 syscall per batch vs 4 syscalls per request (write/read/write/read) -- Batched request processing - -**Level 3: Shared Memory (~0.1ms savings)** -- Results written to `memfd_create` + `mmap` shared region -- Client and daemon map same file descriptor -- Eliminates 2 kernel copies (workerβ†’daemonβ†’client becomes workerβ†’shared memory) -- Request/response arenas in shared memory - -**Level 4: SECCOMP_USER_NOTIF (no worker recycling)** -- Blocked syscalls notify supervisor instead of killing process -- Supervisor returns `EACCES` to continue execution -- Workers don't die on denied syscalls, no recycling needed -- Graceful degradation vs fatal termination - -### Isolation Layers (Defense in Depth) -1. **Linux namespaces** via `clone3` (user, pid, mount, net, ipc, uts) -2. **seccomp user notifications** (`SECCOMP_USER_NOTIF`) - supervisor-mediated syscall filtering -3. **Landlock** filesystem restrictions (whitelist-based) -4. **cgroups v2** resource limits via `CLONE_INTO_CGROUP` (256MB RAM, 100% CPU, 32 PIDs, 30s timeout) - -### Communication Protocol -- **Client ↔ Daemon**: io_uring submission queue or Unix socket fallback -- **Daemon ↔ Worker**: Anonymous pipes for code delivery -- **Results**: Shared memory region (memfd) mapped by both daemon and client -- **Serialization**: MessagePack for control messages -- **Worker lifecycle**: Long-lived, survives denied syscalls (SECCOMP_USER_NOTIF) - -## Workspace Structure - -``` -crates/ -β”œβ”€β”€ leeward-core/ # Core isolation primitives -β”‚ └── src/ -β”‚ β”œβ”€β”€ config.rs # SandboxConfig with resource limits -β”‚ β”œβ”€β”€ worker.rs # Worker process management -β”‚ β”œβ”€β”€ result.rs # ExecutionResult, ExecutionMetrics -β”‚ β”œβ”€β”€ error.rs # Error types -β”‚ └── isolation/ # Isolation mechanisms -β”‚ β”œβ”€β”€ namespace.rs # setup_namespaces() -β”‚ β”œβ”€β”€ seccomp.rs # setup_seccomp() -β”‚ β”œβ”€β”€ landlock.rs # setup_landlock() -β”‚ β”œβ”€β”€ cgroups.rs # setup_cgroups() -β”‚ └── mounts.rs # setup_mounts() -β”œβ”€β”€ leeward-daemon/ # Persistent daemon -β”‚ └── src/ -β”‚ β”œβ”€β”€ main.rs # Entry point, signal handling -β”‚ β”œβ”€β”€ config.rs # DaemonConfig (pool size, socket path) -β”‚ β”œβ”€β”€ pool.rs # WorkerPool management -β”‚ β”œβ”€β”€ server.rs # UnixServer for client connections -β”‚ └── protocol.rs # Request/Response types -β”œβ”€β”€ leeward-cli/ # CLI interface -β”‚ └── src/ -β”‚ └── main.rs # Commands: exec, status, ping, run -└── leeward-ffi/ # C FFI bindings - └── src/ - └── lib.rs # leeward_execute(), leeward_free_result() -``` - -## Build System - -### Cargo (Primary) -```bash -# Development -cargo build -cargo test -cargo check -cargo clippy - -# Aliases (from .cargo/config.toml) -cargo b # build -cargo br # build --release -cargo t # test -cargo c # check -cargo cl # clippy -cargo daemon # run daemon in release mode -cargo cli # run CLI in release mode - -# Release build (with LTO, single codegen unit, stripped) -cargo build --release - -# Test specific crate -cargo test -p leeward-core -cargo test --workspace -``` - -### Nix (Optional, for reproducible builds) -```bash -# Build specific targets -nix build .#cli # CLI binary -nix build .#daemon # Daemon binary -nix build .#ffi # C FFI library (.so and .a) -nix build # Default: all targets - -# Development shell (includes Rust, mold linker, all deps) -nix develop -``` - -### Build Configuration -- **Rust version:** 1.85 (edition 2024) -- **Linker:** mold (via clang) for faster builds -- **Release optimizations:** LTO=fat, codegen-units=1, strip=true, opt-level=3 -- **Dependencies optimized** at opt-level=3 even in dev builds (for package.*) - -## Development Commands - -### Running the daemon -```bash -cargo daemon -# Or with args: -cargo run --release --bin leeward-daemon -- --pool-size 10 -``` - -### Using the CLI -```bash -cargo cli exec "print('hello world')" -cargo cli status -cargo cli ping -``` - -### Testing -```bash -cargo test # All tests -cargo test --workspace # Explicit workspace tests -cargo test -p leeward-core # Single crate -``` - -Test locations: -- `tests/integration/` - Integration tests -- `tests/escapes/` - Security/escape tests -- Unit tests within each crate's `src/` files - -## Key Technical Details - -### Requirements -- Linux >= 5.13 (Landlock support required) -- User namespaces enabled (check: `unshare -U whoami`) -- No root required - -### Default Resource Limits -```rust -// See crates/leeward-core/src/config.rs -memory_limit: 256MB -cpu_quota: 100% -max_processes: 32 -timeout: 30s -``` - -### Seccomp Syscall Whitelist -Only essential Python syscalls allowed. See `crates/leeward-core/src/isolation/seccomp.rs` for the full list (currently marked TODO). - -### Multi-Language Support -The C FFI library (`leeward-ffi`) enables bindings for any language with C interop: -- Python (planned in `bindings/python/`) -- Go, Node.js, Rust - all can use the FFI -- Generated header file via cbindgen -- Both shared (`.so`) and static (`.a`) libraries built - -## Important Implementation Notes - -### Currently Marked TODO -Implementing new execution paradigm: -- `crates/leeward-core/src/isolation/*.rs` - All setup functions -- Pre-fork pool with `clone3` + `CLONE_INTO_CGROUP` -- Pipe-based code delivery to idle workers -- io_uring integration for IPC -- Shared memory (memfd + mmap) for results -- SECCOMP_USER_NOTIF for syscall supervision - -When implementing, maintain the defense-in-depth approach: all 4 isolation layers must work together. - -### Performance Implementation Details - -**Pre-fork Pool:** -- Use `clone3` syscall with `CLONE_INTO_CGROUP` flag -- Workers created at daemon startup, not on-demand -- Each worker loads Python interpreter and enters idle loop -- Communication via anonymous pipes (one per worker) -- Workers never exit unless recycled (every N executions or on error) - -**io_uring Integration:** -- Daemon maintains io_uring instance for client communication -- Submission queue batches multiple requests -- Zero-copy buffers using registered buffers -- Completion queue processed asynchronously -- Fallback to Unix socket if io_uring unavailable - -**Shared Memory:** -- Create with `memfd_create("leeward_results", MFD_CLOEXEC)` -- Request arena: fixed-size slots for incoming code -- Response arena: variable-size slots for stdout/stderr -- Client maps read-only, daemon/workers map read-write -- Ring buffer or slab allocator for slot management - -**SECCOMP_USER_NOTIF:** -- Set up seccomp filter with `SECCOMP_RET_USER_NOTIF` for blocked syscalls -- Supervisor thread polls seccomp notification fd -- On notification: log syscall, return `EACCES` to continue -- Worker continues execution instead of dying -- Reduces worker churn dramatically - -### Security Considerations -- No network access by default (isolated network namespace) -- Filesystem access via Landlock whitelist only -- Resource limits enforced via cgroups v2 -- Syscalls mediated by supervisor (SECCOMP_USER_NOTIF) -- Workers are long-lived but fully isolated -- Shared memory regions are per-request, isolated between executions - -### Code Style -- Workspace uses Rust 2024 edition -- Lints: clippy::all, clippy::pedantic, clippy::nursery enabled -- Unsafe code generates warnings (required for isolation primitives) -- Use `thiserror` for error types -- Use `tracing` for logging, not `println!` - -## License and Support - -- **License:** MIT (previously Apache-2.0, changed in recent commits) -- **Repository:** https://github.com/vektia/leeward -- **Production usage:** See ADOPTION.md to report usage -- **Enterprise support:** hello@vektia.com.br -- **Sponsors:** github.com/sponsors/vektia diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3ddd17b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,277 @@ +# Contributing to Leeward + +Thank you for your interest in contributing to Leeward! This document provides guidelines and information for contributors. + +## πŸš€ Quick Start + +### Development Setup + +#### With Nix (Recommended) +```bash +# Clone the repository +git clone https://github.com/vektia/leeward +cd leeward + +# Enter development shell (auto-loads all dependencies) +nix develop + +# Or with direnv (automatic) +direnv allow +``` + +#### Without Nix +```bash +# Install Rust 1.85+ +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + +# Clone and build +git clone https://github.com/vektia/leeward +cd leeward +cargo build --release +``` + +### Running Locally + +```bash +# Start the daemon +./target/release/leeward-daemon & + +# Or with cargo alias +cargo daemon & + +# Execute code +./target/release/leeward exec "print('Hello, World!')" + +# Or with cargo alias +cargo cli exec "print('Hello, World!')" +``` + +## πŸ—οΈ Architecture Overview + +Leeward uses a pre-fork pool architecture for ultra-low latency (~0.5ms): + +``` +Client (any language) ←→ Unix Socket ←→ Daemon ←→ Pre-warmed Workers + β”œβ”€ Python ready + β”œβ”€ Isolated + └─ Waiting +``` + +### Key Components + +- **leeward-core**: Core isolation primitives (namespaces, seccomp, landlock) +- **leeward-daemon**: Persistent daemon managing worker pool +- **leeward-cli**: Command-line interface +- **leeward-ffi**: C FFI for language bindings + +### Isolation Layers + +1. **Linux Namespaces** - Process, network, mount isolation +2. **Seccomp** - Syscall filtering +3. **Landlock** - Filesystem access control + +## πŸ“ Code Style + +### Rust Guidelines + +- Use Rust 2024 edition +- Enable clippy lints: `clippy::all`, `clippy::pedantic`, `clippy::nursery` +- Use `thiserror` for error types +- Use `tracing` for logging (not `println!`) +- Document public APIs with examples + +### Running Checks + +```bash +# Format code +cargo fmt + +# Run clippy +cargo clippy --all-targets --all-features + +# Run tests +cargo test --workspace + +# Check everything +cargo check && cargo clippy && cargo test +``` + +## πŸ§ͺ Testing + +### Unit Tests +Place unit tests in the same file as the code: +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_something() { + // ... + } +} +``` + +### Integration Tests +Place integration tests in `tests/integration/`: +```rust +// tests/integration/basic_execution.rs +#[test] +fn test_python_execution() { + // ... +} +``` + +### Security Tests +Security/escape tests go in `tests/escapes/`: +```rust +// tests/escapes/network_escape.rs +#[test] +fn test_network_isolation() { + // Should fail - no network in sandbox +} +``` + +## πŸ”§ Making Changes + +### 1. Fork and Branch +```bash +git checkout -b feature/your-feature +# or +git checkout -b fix/your-bugfix +``` + +### 2. Make Your Changes +- Write clean, documented code +- Add tests for new functionality +- Update documentation if needed + +### 3. Commit Guidelines +Follow conventional commits: +``` +feat: add new feature +fix: fix bug in worker pool +docs: update README +test: add security tests +refactor: simplify isolation code +perf: optimize worker spawning +``` + +### 4. Submit PR +- Fill out the PR template +- Ensure CI passes +- Wait for review + +## 🚒 Release Process + +### Building Release Artifacts + +#### Local Build with Nix +```bash +# Build all components +nix build .#daemon +nix build .#cli + +# Build Debian package +nix build -f nix/deb.nix + +# Create static binary +cargo build --release --target x86_64-unknown-linux-musl +``` + +#### GitHub Actions +Releases are automatically built when pushing tags: +```bash +git tag v0.1.0 +git push origin v0.1.0 +``` + +This creates: +- Linux binaries (x86_64, aarch64) +- Debian packages (.deb) +- Static musl binaries +- Nix bundles +- SHA256 checksums + +### Using the Nix Overlay + +```nix +# In your flake.nix +{ + inputs.leeward.url = "github:vektia/leeward"; + + outputs = { self, nixpkgs, leeward, ... }: { + nixosConfigurations.myhost = nixpkgs.lib.nixosSystem { + modules = [ + { + nixpkgs.overlays = [ leeward.overlays.default ]; + environment.systemPackages = [ pkgs.leeward ]; + } + ]; + }; + }; +} +``` + +## πŸ“‹ TODO for v1.0 + +High priority items for the v1.0 release: + +- [ ] Implement io_uring IPC for zero-copy communication +- [ ] Add shared memory support (memfd + mmap) +- [ ] Implement SECCOMP_USER_NOTIF for syscall supervision +- [ ] Complete Python bindings +- [ ] Add timeout handling in workers +- [ ] Implement worker recycling after N executions +- [ ] Add metrics/monitoring +- [ ] Complete FFI for other languages + +## πŸ› Debugging + +### Enable Debug Logging +```bash +RUST_LOG=debug ./target/release/leeward-daemon +``` + +### Check Worker Status +```bash +./target/release/leeward status +``` + +### Common Issues + +**"Operation not permitted" on namespaces** +```bash +# Check if user namespaces are enabled +cat /proc/sys/kernel/unprivileged_userns_clone +# Should be 1 +``` + +**Landlock not available** +```bash +# Need Linux >= 5.13 +uname -r +``` + +## πŸ“š Resources + +- [Linux Namespaces](https://man7.org/linux/man-pages/man7/namespaces.7.html) +- [Seccomp](https://man7.org/linux/man-pages/man2/seccomp.2.html) +- [Landlock](https://docs.kernel.org/userspace-api/landlock.html) +- [io_uring](https://kernel.dk/io_uring.pdf) + +## πŸ“„ License + +By contributing, you agree that your contributions will be licensed under the Apache-2.0 License. + +## 🀝 Code of Conduct + +Be respectful, inclusive, and constructive. We're building secure software that protects users - let's do it together professionally. + +## πŸ’¬ Questions? + +- Open an issue for bugs/features +- Start a discussion for questions +- Email: hello@vektia.com.br + +Happy coding! πŸš€ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 315b0db..2e74435 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,9 +3,9 @@ resolver = "2" members = ["crates/*"] [workspace.package] -version = "0.0.1" +version = "0.1.0" edition = "2024" -license = "MIT" +license = "Apache-2.0" repository = "https://github.com/vektia/leeward" authors = ["Vektia"] rust-version = "1.85" diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..969a42c --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,325 @@ +# Installation Guide + +## Quick Install + +### From Release (Binary) + +Download the latest release from [GitHub Releases](https://github.com/vektia/leeward/releases): + +```bash +# Linux x86_64 +curl -L https://github.com/vektia/leeward/releases/latest/download/leeward-amd64-linux.tar.gz | tar xz +sudo mv leeward/leeward* /usr/local/bin/ + +# Linux ARM64 +curl -L https://github.com/vektia/leeward/releases/latest/download/leeward-arm64-linux.tar.gz | tar xz +sudo mv leeward/leeward* /usr/local/bin/ +``` + +### Debian/Ubuntu + +```bash +# Download and install the .deb package +wget https://github.com/vektia/leeward/releases/latest/download/leeward_*_amd64.deb +sudo dpkg -i leeward_*.deb + +# The daemon will be installed as a systemd service +sudo systemctl start leeward +sudo systemctl enable leeward +``` + +### Nix (Flakes) + +Add to your `flake.nix`: + +```nix +{ + inputs = { + leeward.url = "github:vektia/leeward"; + }; + + outputs = { self, nixpkgs, leeward, ... }: { + # Use as overlay + nixpkgs.overlays = [ leeward.overlays.default ]; + + # Or add to system packages + environment.systemPackages = [ leeward.packages.${system}.default ]; + }; +} +``` + +Direct installation: + +```bash +# Install CLI and daemon +nix profile install github:vektia/leeward#cli +nix profile install github:vektia/leeward#daemon + +# Or run directly +nix run github:vektia/leeward#daemon +nix run github:vektia/leeward#cli -- exec "print('hello')" +``` + +### From Source + +```bash +# Clone repository +git clone https://github.com/vektia/leeward +cd leeward + +# Build with Cargo +cargo build --release + +# Install binaries +sudo cp target/release/leeward-daemon /usr/local/bin/ +sudo cp target/release/leeward /usr/local/bin/ + +# Or build with Nix +nix build .#daemon +nix build .#cli +``` + +## Platform-Specific Instructions + +### NixOS + +Add to your `configuration.nix`: + +```nix +{ config, pkgs, ... }: +{ + # Import the module + imports = [ + "${builtins.fetchTarball "https://github.com/vektia/leeward/archive/main.tar.gz"}/nix/module.nix" + ]; + + # Enable the service + services.leeward = { + enable = true; + workers = 4; # Number of pre-forked workers + }; + + # Enable user namespaces + boot.kernel.sysctl."kernel.unprivileged_userns_clone" = 1; +} +``` + +### Ubuntu/Debian + +```bash +# Enable user namespaces (if not already enabled) +echo 'kernel.unprivileged_userns_clone=1' | sudo tee /etc/sysctl.d/99-userns.conf +sudo sysctl --system + +# Install from .deb +wget https://github.com/vektia/leeward/releases/latest/download/leeward_*_amd64.deb +sudo dpkg -i leeward_*.deb + +# Start service +sudo systemctl enable --now leeward +``` + +### Fedora/RHEL/Rocky + +```bash +# Enable user namespaces (if disabled) +sudo sysctl -w kernel.unprivileged_userns_clone=1 +echo 'kernel.unprivileged_userns_clone=1' | sudo tee /etc/sysctl.d/99-userns.conf + +# Install from tarball +curl -L https://github.com/vektia/leeward/releases/latest/download/leeward-amd64-linux.tar.gz | tar xz +sudo mv leeward/* /usr/local/bin/ + +# Create systemd service +sudo cp leeward/leeward.service /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable --now leeward +``` + +### Arch Linux + +```bash +# User namespaces are enabled by default + +# Install from AUR (when available) +yay -S leeward + +# Or from tarball +curl -L https://github.com/vektia/leeward/releases/latest/download/leeward-amd64-linux.tar.gz | tar xz +sudo mv leeward/* /usr/local/bin/ +``` + +### Alpine Linux (Static Binary) + +```bash +# Download static musl binary +wget https://github.com/vektia/leeward/releases/latest/download/leeward-static-linux-amd64.tar.gz +tar xzf leeward-static-linux-amd64.tar.gz +sudo mv leeward-static/* /usr/local/bin/ +``` + +## Running as a Service + +### Systemd (System Service) + +```bash +# Copy the service file +sudo cp contrib/leeward.system.service /etc/systemd/system/leeward.service + +# Create leeward user +sudo useradd -r -s /usr/sbin/nologin -d /nonexistent leeward + +# Enable and start +sudo systemctl daemon-reload +sudo systemctl enable --now leeward + +# Check status +sudo systemctl status leeward +journalctl -u leeward -f +``` + +### Systemd (User Service) + +```bash +# Copy user service file +mkdir -p ~/.config/systemd/user/ +cp contrib/leeward.user.service ~/.config/systemd/user/leeward.service + +# Start user service +systemctl --user daemon-reload +systemctl --user enable --now leeward + +# Check status +systemctl --user status leeward +``` + +### Docker (Alternative) + +If you prefer containerized deployment: + +```bash +# Using the official image (when available) +docker run -d \ + --privileged \ + --name leeward \ + -v /run/leeward:/run/leeward \ + ghcr.io/vektia/leeward:latest + +# Or build from source +docker build -t leeward . +docker run -d --privileged --name leeward leeward +``` + +## Verification + +After installation, verify everything works: + +```bash +# Check daemon is running +leeward status + +# Execute test code +leeward exec "print('Hello from Leeward!')" + +# Test isolation (should fail) +leeward exec "import socket; socket.socket()" # No network +leeward exec "open('/etc/passwd', 'r')" # No filesystem access +``` + +## Building Packages + +### Build Debian Package + +With Nix: +```bash +nix build -f nix/deb.nix +# Output: result/leeward_0.1.0_amd64.deb +``` + +### Build Static Binary + +```bash +# Install musl target +rustup target add x86_64-unknown-linux-musl + +# Build static binary +cargo build --release --target x86_64-unknown-linux-musl +``` + +### Build All Release Artifacts + +```bash +# With Nix (recommended) +nix build .#daemon +nix build .#cli +nix build -f nix/deb.nix + +# With Cargo +cargo build --release +cargo build --release --target x86_64-unknown-linux-musl +``` + +## Troubleshooting + +### "Operation not permitted" on namespaces + +Check if user namespaces are enabled: +```bash +cat /proc/sys/kernel/unprivileged_userns_clone +# Should output: 1 + +# If not, enable: +sudo sysctl -w kernel.unprivileged_userns_clone=1 +``` + +### Landlock not available + +Requires Linux kernel >= 5.13: +```bash +uname -r # Check kernel version + +# Check if Landlock is enabled +cat /sys/kernel/security/lsm | grep landlock +``` + +### Socket permission denied + +Ensure the socket directory exists and has correct permissions: +```bash +# For system service +sudo mkdir -p /run/leeward +sudo chown leeward:leeward /run/leeward + +# For user service +mkdir -p $XDG_RUNTIME_DIR/leeward +``` + +## Uninstall + +### From Package Manager +```bash +# Debian/Ubuntu +sudo apt remove leeward + +# With systemctl +sudo systemctl stop leeward +sudo systemctl disable leeward +``` + +### Manual Uninstall +```bash +# Stop service +sudo systemctl stop leeward + +# Remove binaries +sudo rm /usr/local/bin/leeward* + +# Remove service files +sudo rm /etc/systemd/system/leeward.service + +# Remove user (if created) +sudo userdel leeward + +# Remove runtime directory +sudo rm -rf /run/leeward +``` \ No newline at end of file diff --git a/README.md b/README.md index b0f462e..c88b5e4 100644 --- a/README.md +++ b/README.md @@ -1,91 +1,36 @@ # leeward -> Linux-native sandbox for running untrusted code. No containers. No VMs. Fast. - -⚠️ **Work in progress** β€” Core isolation primitives are being implemented. - -## Why - -AI agents need to execute code. Current options suck: - -| Solution | Problem | -|----------|---------| -| Docker | 300-500ms startup, heavy | -| E2B/Modal | Cloud-only, expensive | -| WASM | No native libs, limited | -| Firecracker | Overkill for most cases | - -leeward gives you **~0.5ms** execution latency using native Linux primitives. - -## How -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” io_uring/shm β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Client β”‚ ◄───────────────► β”‚ leeward daemon β”‚ -β”‚ (any lang) β”‚ zero-copy IPC β”‚ β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ - β”‚ β”‚ Pre-warmed Worker Pool β”‚ β”‚ - Python, Go, β”‚ β”‚ β”‚ β”‚ - Node, Rust β”‚ β”‚ [W1] Python idle ──pipe β”‚ β”‚ - via C FFI β”‚ β”‚ [W2] Python idle ──pipe β”‚ β”‚ - β”‚ β”‚ [W3] Python idle ──pipe β”‚ β”‚ - β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` - -Each worker is isolated with: -- Linux namespaces (user, pid, mount, net, ipc) via clone3 -- seccomp user notifications (supervisor decides on blocked syscalls) -- Landlock filesystem restrictions -- cgroups v2 resource limits (CLONE_INTO_CGROUP) +Linux-native sandbox for running untrusted code with ~0.5ms latency. ## Usage + ```python from leeward import Leeward with Leeward() as sandbox: - result = sandbox.execute("print(sum(range(100)))") - print(result.stdout) # "4950" -``` -```bash -# Or via CLI -leeward exec "print('hello')" + result = sandbox.execute("print('Hello, World!')") + print(result.stdout) # "Hello, World!" ``` +## Features + +- **Fast**: ~0.5ms execution latency (vs Docker's 300-500ms) +- **Secure**: Linux namespaces + seccomp + Landlock +- **Simple**: No containers or VMs needed +- **Lightweight**: Pre-forked Python workers + ## Requirements -- Linux >= 5.13 (Landlock support) +- Linux >= 5.13 - User namespaces enabled - No root required -## Development - -```bash -# With direnv (auto-loads environment) -direnv allow - -# Or manually -nix develop - -# Build and run -cargo build --release -./target/release/leeward-daemon & # runs in background -./target/release/leeward exec "print('hello')" -``` - -Environment sets `LEEWARD_SOCKET=$DEVENV_STATE/leeward.sock` for isolation between clones. - -## Status - -Building the core. Not ready for production. - -## Support - -leeward is free and open source under [Apache-2.0](LICENSE.md). +## Documentation -- **Using in production?** [Let us know](ADOPTION.md) β€” it helps the project -- **Sponsors** β€” [github.com/sponsors/vektia](https://github.com/sponsors/vektia) -- **Enterprise support** β€” hello@vektia.com.br +- [Installation](INSTALL.md) +- [Contributing](CONTRIBUTING.md) +- [Changelog](CHANGELOG.md) ## License -[Apache-2.0](LICENSE.md) \ No newline at end of file +Apache-2.0 \ No newline at end of file diff --git a/contrib/leeward.system.service b/contrib/leeward.system.service new file mode 100644 index 0000000..c1950f4 --- /dev/null +++ b/contrib/leeward.system.service @@ -0,0 +1,26 @@ +[Unit] +Description=Leeward Sandbox Daemon +After=network.target + +[Service] +Type=simple +ExecStart=/usr/local/bin/leeward-daemon +Restart=always +RestartSec=5 +User=leeward +Group=leeward + +# Security hardening +NoNewPrivileges=true +ProtectSystem=strict +ProtectHome=true +PrivateTmp=true +RuntimeDirectory=leeward +RuntimeDirectoryMode=0755 + +# Resource limits +MemoryMax=1G +CPUQuota=200% + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/contrib/leeward.user.service b/contrib/leeward.user.service new file mode 100644 index 0000000..bebd595 --- /dev/null +++ b/contrib/leeward.user.service @@ -0,0 +1,14 @@ +[Unit] +Description=Leeward Sandbox Daemon (User) +After=default.target + +[Service] +Type=simple +ExecStart=%h/.local/bin/leeward-daemon +Restart=always +RestartSec=5 +RuntimeDirectory=leeward +RuntimeDirectoryMode=0700 + +[Install] +WantedBy=default.target \ No newline at end of file diff --git a/crates/leeward-cli/src/main.rs b/crates/leeward-cli/src/main.rs index 19f93d1..26eef33 100644 --- a/crates/leeward-cli/src/main.rs +++ b/crates/leeward-cli/src/main.rs @@ -61,10 +61,6 @@ enum Commands { /// Timeout in seconds #[arg(short, long, default_value = "30")] timeout: u64, - - /// Memory limit in MB - #[arg(short, long, default_value = "256")] - memory: u64, }, /// Get daemon status @@ -90,10 +86,6 @@ enum Commands { #[arg(short, long, default_value = "30")] timeout: u64, - /// Memory limit in MB - #[arg(short, long, default_value = "256")] - memory: u64, - /// Allow network access #[arg(long)] network: bool, @@ -116,7 +108,6 @@ async fn main() -> Result<(), Box> { code, socket, timeout, - memory, } => { let socket = socket.unwrap_or_else(default_socket_path); @@ -125,7 +116,7 @@ async fn main() -> Result<(), Box> { code: Some(code), shm_slot_id: None, timeout: Some(std::time::Duration::from_secs(timeout)), - memory_limit: Some(memory * 1024 * 1024), + memory_limit: None, files: Vec::new(), } ); @@ -195,20 +186,18 @@ async fn main() -> Result<(), Box> { Commands::Run { code, timeout, - memory, network, } => { println!("Running directly (no daemon)"); println!("Code: {}", code); println!( - "Timeout: {}s, Memory: {}MB, Network: {}", - timeout, memory, network + "Timeout: {}s, Network: {}", + timeout, network ); // TODO: Use leeward_core directly to execute let _config = leeward_core::SandboxConfig::builder() .timeout_secs(timeout) - .memory_limit_mb(memory) .allow_network(network) .build(); } diff --git a/crates/leeward-core/src/config.rs b/crates/leeward-core/src/config.rs index d4c2266..4fa6ce9 100644 --- a/crates/leeward-core/src/config.rs +++ b/crates/leeward-core/src/config.rs @@ -16,18 +16,9 @@ pub struct SandboxConfig { /// Paths to bind mount read-write pub rw_binds: Vec, - /// Memory limit in bytes - pub memory_limit: u64, - - /// CPU limit as percentage (0-100) - pub cpu_limit: u32, - /// Maximum execution time pub timeout: Duration, - /// Maximum number of processes/threads - pub max_pids: u32, - /// Allow network access pub allow_network: bool, @@ -48,10 +39,7 @@ impl Default for SandboxConfig { PathBuf::from("/lib64"), ], rw_binds: vec![], - memory_limit: 256 * 1024 * 1024, // 256MB - cpu_limit: 100, timeout: Duration::from_secs(30), - max_pids: 32, allow_network: false, workdir: PathBuf::from("/home/sandbox"), env: vec![ @@ -84,23 +72,6 @@ impl SandboxConfigBuilder { self } - #[must_use] - pub fn memory_limit(mut self, bytes: u64) -> Self { - self.config.memory_limit = bytes; - self - } - - #[must_use] - pub fn memory_limit_mb(self, mb: u64) -> Self { - self.memory_limit(mb * 1024 * 1024) - } - - #[must_use] - pub fn cpu_limit(mut self, percent: u32) -> Self { - self.config.cpu_limit = percent.min(100); - self - } - #[must_use] pub fn timeout(mut self, duration: Duration) -> Self { self.config.timeout = duration; diff --git a/crates/leeward-core/src/error.rs b/crates/leeward-core/src/error.rs index dc28022..cdad496 100644 --- a/crates/leeward-core/src/error.rs +++ b/crates/leeward-core/src/error.rs @@ -13,9 +13,6 @@ pub enum LeewardError { #[error("landlock error: {0}")] Landlock(String), - #[error("cgroups error: {0}")] - Cgroups(String), - #[error("mount error: {0}")] Mount(String), diff --git a/crates/leeward-core/src/isolation/cgroups.rs b/crates/leeward-core/src/isolation/cgroups.rs deleted file mode 100644 index 1f12182..0000000 --- a/crates/leeward-core/src/isolation/cgroups.rs +++ /dev/null @@ -1,241 +0,0 @@ -//! Cgroups v2 resource limits - -use crate::{LeewardError, Result}; -use std::fs; -use std::path::{Path, PathBuf}; -use std::os::unix::io::{AsRawFd, RawFd}; - -/// Configuration for cgroups v2 resource limits -#[derive(Debug, Clone)] -pub struct CgroupsConfig { - /// Memory limit in bytes (memory.max) - pub memory_max: u64, - /// CPU quota as percentage (cpu.max) - pub cpu_percent: u32, - /// Maximum number of processes (pids.max) - pub pids_max: u32, - /// Enable memory swap (memory.swap.max) - pub allow_swap: bool, -} - -impl Default for CgroupsConfig { - fn default() -> Self { - Self { - memory_max: 256 * 1024 * 1024, // 256MB - cpu_percent: 100, - pids_max: 32, - allow_swap: false, - } - } -} - -impl CgroupsConfig { - /// Create a new cgroup for a sandbox - pub fn create_cgroup(&self, name: &str) -> Result { - tracing::debug!( - name, - memory = self.memory_max, - cpu = self.cpu_percent, - pids = self.pids_max, - "creating cgroup" - ); - - let cgroup_root = PathBuf::from("/sys/fs/cgroup"); - let leeward_cgroup = cgroup_root.join("leeward"); - let cgroup_path = leeward_cgroup.join(name); - - // Ensure leeward parent cgroup exists - if !leeward_cgroup.exists() { - fs::create_dir_all(&leeward_cgroup) - .map_err(|e| LeewardError::Cgroups(format!("failed to create leeward cgroup: {e}")))?; - - // Enable controllers in leeward cgroup - enable_controllers(&cgroup_root, "leeward")?; - } - - // Create the specific cgroup - if !cgroup_path.exists() { - fs::create_dir_all(&cgroup_path) - .map_err(|e| LeewardError::Cgroups(format!("failed to create cgroup {}: {e}", name)))?; - } - - // Set memory limit - let memory_max_path = cgroup_path.join("memory.max"); - fs::write(&memory_max_path, self.memory_max.to_string()) - .map_err(|e| LeewardError::Cgroups(format!("failed to set memory.max: {e}")))?; - - // Set memory swap limit (0 = no swap) - if !self.allow_swap { - let swap_max_path = cgroup_path.join("memory.swap.max"); - fs::write(&swap_max_path, "0") - .map_err(|e| LeewardError::Cgroups(format!("failed to set memory.swap.max: {e}")))?; - } - - // Set CPU limit (percentage as quota/period) - // cpu.max format: "$quota $period" or "max $period" - let cpu_max_path = cgroup_path.join("cpu.max"); - let period = 100000; // 100ms in microseconds - let quota = if self.cpu_percent >= 100 { - "max".to_string() - } else { - ((self.cpu_percent as u64 * period) / 100).to_string() - }; - fs::write(&cpu_max_path, format!("{} {}", quota, period)) - .map_err(|e| LeewardError::Cgroups(format!("failed to set cpu.max: {e}")))?; - - // Set PIDs limit - let pids_max_path = cgroup_path.join("pids.max"); - fs::write(&pids_max_path, self.pids_max.to_string()) - .map_err(|e| LeewardError::Cgroups(format!("failed to set pids.max: {e}")))?; - - // Get FD for CLONE_INTO_CGROUP - let cgroup_fd = fs::File::open(&cgroup_path) - .map_err(|e| LeewardError::Cgroups(format!("failed to open cgroup: {e}")))? - .as_raw_fd(); - - Ok(CgroupHandle { - name: name.to_string(), - path: cgroup_path, - fd: Some(cgroup_fd), - }) - } -} - -/// Enable controllers in a cgroup -fn enable_controllers(parent: &Path, child_name: &str) -> Result<()> { - let subtree_control = parent.join("cgroup.subtree_control"); - - // Read current controllers - let current = fs::read_to_string(&subtree_control).unwrap_or_default(); - - // Enable memory, cpu, and pids controllers if not already enabled - let mut controllers = vec![]; - if !current.contains("memory") { - controllers.push("+memory"); - } - if !current.contains("cpu") { - controllers.push("+cpu"); - } - if !current.contains("pids") { - controllers.push("+pids"); - } - - if !controllers.is_empty() { - let control_str = controllers.join(" "); - fs::write(&subtree_control, control_str) - .map_err(|e| LeewardError::Cgroups(format!( - "failed to enable controllers for {}: {e}", - child_name - )))?; - } - - Ok(()) -} - -/// Handle to a cgroup -#[derive(Debug)] -pub struct CgroupHandle { - name: String, - path: PathBuf, - fd: Option, -} - -impl CgroupHandle { - /// Get the file descriptor for CLONE_INTO_CGROUP - pub fn as_raw_fd(&self) -> Option { - self.fd - } - - /// Add a process to this cgroup - pub fn add_process(&self, pid: u32) -> Result<()> { - tracing::debug!(cgroup = %self.name, pid, "adding process to cgroup"); - - let procs_path = self.path.join("cgroup.procs"); - fs::write(&procs_path, pid.to_string()) - .map_err(|e| LeewardError::Cgroups(format!("failed to add process to cgroup: {e}")))?; - - Ok(()) - } - - /// Get current memory usage - pub fn memory_current(&self) -> Result { - let memory_current_path = self.path.join("memory.current"); - let content = fs::read_to_string(&memory_current_path) - .map_err(|e| LeewardError::Cgroups(format!("failed to read memory.current: {e}")))?; - - content - .trim() - .parse() - .map_err(|e| LeewardError::Cgroups(format!("failed to parse memory.current: {e}"))) - } - - /// Get peak memory usage - pub fn memory_peak(&self) -> Result { - let memory_peak_path = self.path.join("memory.peak"); - let content = fs::read_to_string(&memory_peak_path) - .map_err(|e| LeewardError::Cgroups(format!("failed to read memory.peak: {e}")))?; - - content - .trim() - .parse() - .map_err(|e| LeewardError::Cgroups(format!("failed to parse memory.peak: {e}"))) - } - - /// Check if OOM killed - pub fn was_oom_killed(&self) -> Result { - let events_path = self.path.join("memory.events"); - let content = fs::read_to_string(&events_path) - .map_err(|e| LeewardError::Cgroups(format!("failed to read memory.events: {e}")))?; - - // Parse events file for oom_kill count - for line in content.lines() { - if let Some(oom_line) = line.strip_prefix("oom_kill ") { - let count: u64 = oom_line - .parse() - .map_err(|e| LeewardError::Cgroups(format!("failed to parse oom_kill count: {e}")))?; - return Ok(count > 0); - } - } - - Ok(false) - } - - /// Get CPU usage statistics - pub fn cpu_stat(&self) -> Result<(u64, u64)> { - let cpu_stat_path = self.path.join("cpu.stat"); - let content = fs::read_to_string(&cpu_stat_path) - .map_err(|e| LeewardError::Cgroups(format!("failed to read cpu.stat: {e}")))?; - - let mut usage_usec = 0u64; - let mut user_usec = 0u64; - - for line in content.lines() { - if let Some(usage) = line.strip_prefix("usage_usec ") { - usage_usec = usage.parse() - .map_err(|e| LeewardError::Cgroups(format!("failed to parse usage_usec: {e}")))?; - } else if let Some(user) = line.strip_prefix("user_usec ") { - user_usec = user.parse() - .map_err(|e| LeewardError::Cgroups(format!("failed to parse user_usec: {e}")))?; - } - } - - Ok((usage_usec, user_usec)) - } - - /// Destroy the cgroup - pub fn destroy(self) -> Result<()> { - tracing::debug!(cgroup = %self.name, "destroying cgroup"); - - // Close the file descriptor first if we have one - if let Some(fd) = self.fd { - // SAFETY: closing a file descriptor we own - unsafe { libc::close(fd); } - } - - // Remove the cgroup directory - fs::remove_dir(&self.path) - .map_err(|e| LeewardError::Cgroups(format!("failed to remove cgroup: {e}")))?; - - Ok(()) - } -} diff --git a/crates/leeward-core/src/isolation/clone3.rs b/crates/leeward-core/src/isolation/clone3.rs index b379215..a453f94 100644 --- a/crates/leeward-core/src/isolation/clone3.rs +++ b/crates/leeward-core/src/isolation/clone3.rs @@ -1,8 +1,7 @@ -//! clone3 syscall wrapper with CLONE_INTO_CGROUP support +//! clone3 syscall wrapper for process creation use crate::{LeewardError, Result}; use libc::pid_t; -use std::os::unix::io::RawFd; /// clone3 clone_args structure (from linux/sched.h) #[repr(C)] @@ -24,16 +23,11 @@ pub struct CloneArgs { pub set_tid: u64, /// Size of set_tid array pub set_tid_size: u64, - /// File descriptor for cgroup - pub cgroup: u64, } /// clone3 syscall number const SYS_CLONE3: i64 = 435; -/// CLONE_INTO_CGROUP flag (requires Linux >= 5.7) -pub const CLONE_INTO_CGROUP: u64 = 0x200000000; - /// Wrapper around the clone3 syscall /// /// # Safety @@ -58,23 +52,14 @@ pub unsafe fn clone3(args: &CloneArgs) -> Result { Ok(ret as pid_t) } -/// Helper to create a pre-forked worker with namespaces and cgroup +/// Helper to create a pre-forked worker with namespaces pub fn clone_worker( - cgroup_fd: RawFd, namespace_flags: u64, child_fn: impl FnOnce() -> Result<()>, ) -> Result { - let mut flags = namespace_flags; - - // Only use CLONE_INTO_CGROUP if we have a valid fd - if cgroup_fd >= 0 { - flags |= CLONE_INTO_CGROUP; - } - let args = CloneArgs { - flags, + flags: namespace_flags, exit_signal: libc::SIGCHLD as u64, - cgroup: if cgroup_fd >= 0 { cgroup_fd as u64 } else { 0 }, ..Default::default() }; diff --git a/crates/leeward-core/src/isolation/mod.rs b/crates/leeward-core/src/isolation/mod.rs index d55e6c3..145f5ff 100644 --- a/crates/leeward-core/src/isolation/mod.rs +++ b/crates/leeward-core/src/isolation/mod.rs @@ -1,21 +1,18 @@ //! Linux isolation primitives //! //! This module contains the core isolation mechanisms: -//! - `clone3` - clone3 syscall with CLONE_INTO_CGROUP support +//! - `clone3` - clone3 syscall for process creation //! - `namespace` - Linux namespaces (user, pid, mount, net, ipc) //! - `seccomp` - syscall filtering with SECCOMP_USER_NOTIF //! - `landlock` - filesystem access control -//! - `cgroups` - resource limits //! - `mounts` - filesystem setup with bind mounts and tmpfs -pub mod cgroups; pub mod clone3; pub mod landlock; pub mod mounts; pub mod namespace; pub mod seccomp; -pub use self::cgroups::CgroupsConfig; pub use self::landlock::LandlockConfig; pub use self::mounts::MountConfig; pub use self::namespace::NamespaceConfig; diff --git a/crates/leeward-core/src/lib.rs b/crates/leeward-core/src/lib.rs index dee6a2b..0ec0543 100644 --- a/crates/leeward-core/src/lib.rs +++ b/crates/leeward-core/src/lib.rs @@ -6,7 +6,6 @@ //! - Linux namespaces via clone3 (user, pid, mount, net, ipc) //! - seccomp user notifications (SECCOMP_USER_NOTIF) //! - Landlock filesystem restrictions -//! - cgroups v2 resource limits (CLONE_INTO_CGROUP) //! - Shared memory for zero-copy results (memfd + mmap) //! - Pipe-based code delivery to pre-forked workers diff --git a/crates/leeward-core/src/worker.rs b/crates/leeward-core/src/worker.rs index 54a8bae..58504b2 100644 --- a/crates/leeward-core/src/worker.rs +++ b/crates/leeward-core/src/worker.rs @@ -1,5 +1,4 @@ use crate::{pipe::ParentPipe, ExecutionResult, LeewardError, Result, SandboxConfig}; -use std::os::unix::io::RawFd; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WorkerState { @@ -21,8 +20,6 @@ pub struct Worker { pub execution_count: u64, config: SandboxConfig, pipe: Option, - /// Cgroup file descriptor (for CLONE_INTO_CGROUP) - cgroup_fd: Option, } impl Worker { @@ -34,44 +31,15 @@ impl Worker { execution_count: 0, config, pipe: None, - cgroup_fd: None, } } pub fn spawn(&mut self) -> Result<()> { - use crate::isolation::{clone3, CgroupsConfig}; + use crate::isolation::clone3; use crate::pipe::WorkerPipe; tracing::info!(worker_id = self.id, "spawning pre-forked worker"); - // Try to create cgroup for this worker (optional - continue if it fails) - let cgroup_fd = if self.config.memory_limit > 0 || self.config.cpu_limit < 100 { - let cgroup_config = CgroupsConfig { - memory_max: self.config.memory_limit, - cpu_percent: self.config.cpu_limit, - pids_max: self.config.max_pids, - allow_swap: false, - }; - - match cgroup_config.create_cgroup(&format!("worker-{}", self.id)) { - Ok(cgroup_handle) => { - let fd = cgroup_handle.as_raw_fd().unwrap_or(-1); - // Store cgroup handle (we'll leak it for now, proper cleanup later) - std::mem::forget(cgroup_handle); - tracing::info!("cgroups enabled for worker {}", self.id); - fd - } - Err(e) => { - tracing::warn!("cgroups not available (running without root?): {}", e); - -1 - } - } - } else { - -1 - }; - - self.cgroup_fd = Some(cgroup_fd); - // Create pipes for communication let worker_pipe = WorkerPipe::new()?; let (parent_pipe, child_pipe) = worker_pipe.split(); @@ -80,7 +48,7 @@ impl Worker { let namespace_flags = 0; // We'll enter namespaces from inside the worker let config = self.config.clone(); - let pid = clone3::clone_worker(cgroup_fd, namespace_flags, move || { + let pid = clone3::clone_worker(namespace_flags, move || { worker_main(child_pipe, &config) })?; @@ -91,7 +59,7 @@ impl Worker { tracing::info!( worker_id = self.id, pid = pid, - "worker spawned and ready with cgroup" + "worker spawned and ready" ); Ok(()) diff --git a/docs/install.md b/docs/install.md deleted file mode 100644 index c392b3e..0000000 --- a/docs/install.md +++ /dev/null @@ -1,278 +0,0 @@ -# Installation Guide - -## Quick Start (No root required) - -leeward works without root, but with reduced isolation (no cgroups resource limits). - -```bash -# Build -cargo build --release - -# Run daemon -./target/release/leeward-daemon & - -# Execute code -./target/release/leeward exec "print('hello')" -``` - -## Full Setup (with cgroups v2) - -For complete isolation with memory/CPU limits (like Docker), you need cgroups v2 configured. - -### 1. Verify cgroups v2 is enabled - -```bash -# Check if cgroups v2 is mounted -mount | grep cgroup2 - -# Should show something like: -# cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime) -``` - -If not mounted, add to kernel boot parameters: -``` -systemd.unified_cgroup_hierarchy=1 -``` - -### 2. Enable cgroup delegation for your user - -#### Option A: Systemd user service (recommended) - -Create `/etc/systemd/system/user@.service.d/delegate.conf`: -```ini -[Service] -Delegate=cpu cpuset io memory pids -``` - -Then reload: -```bash -sudo systemctl daemon-reload -``` - -#### Option B: Run with systemd-run - -```bash -systemd-run --user --scope -p Delegate=yes ./target/release/leeward-daemon -``` - -#### Option C: Configure user cgroup delegation - -```bash -# Add your user to systemd cgroup delegation -sudo mkdir -p /etc/systemd/system/user@$(id -u).service.d/ -sudo tee /etc/systemd/system/user@$(id -u).service.d/delegate.conf << 'EOF' -[Service] -Delegate=cpu cpuset io memory pids -EOF - -sudo systemctl daemon-reload -sudo systemctl restart user@$(id -u).service -``` - -### 3. Verify delegation - -```bash -# Check your user's cgroup -cat /sys/fs/cgroup/user.slice/user-$(id -u).slice/cgroup.controllers -# Should show: cpuset cpu io memory pids -``` - -### 4. Run leeward with full isolation - -```bash -# As root (simplest, for testing) -sudo ./target/release/leeward-daemon - -# Or as user with delegation configured -./target/release/leeward-daemon -``` - -## Distribution-Specific Setup - -### NixOS - -Add to your `configuration.nix`: - -```nix -{ config, pkgs, ... }: -{ - # Enable cgroups v2 (default on modern NixOS) - boot.kernelParams = [ "systemd.unified_cgroup_hierarchy=1" ]; - - # Enable user namespaces - boot.kernel.sysctl."kernel.unprivileged_userns_clone" = 1; - - # Import leeward module - imports = [ - (builtins.fetchTarball "https://github.com/vektia/leeward/archive/main.tar.gz" + "/nix/module.nix") - ]; - - services.leeward = { - enable = true; - # Optional: configure workers, memory limits, etc. - workers = 4; - }; -} -``` - -### Ubuntu/Debian - -```bash -# Enable cgroups v2 (if not already) -sudo sed -i 's/GRUB_CMDLINE_LINUX=""/GRUB_CMDLINE_LINUX="systemd.unified_cgroup_hierarchy=1"/' /etc/default/grub -sudo update-grub -sudo reboot - -# Enable user namespaces -echo 'kernel.unprivileged_userns_clone=1' | sudo tee /etc/sysctl.d/99-userns.conf -sudo sysctl --system - -# Setup cgroup delegation -sudo mkdir -p /etc/systemd/system/user@.service.d/ -sudo tee /etc/systemd/system/user@.service.d/delegate.conf << 'EOF' -[Service] -Delegate=cpu cpuset io memory pids -EOF -sudo systemctl daemon-reload -``` - -### Fedora/RHEL - -```bash -# cgroups v2 is default on Fedora 31+ - -# Enable user namespaces (if disabled) -sudo sysctl -w kernel.unprivileged_userns_clone=1 -echo 'kernel.unprivileged_userns_clone=1' | sudo tee /etc/sysctl.d/99-userns.conf - -# Setup cgroup delegation -sudo mkdir -p /etc/systemd/system/user@.service.d/ -sudo tee /etc/systemd/system/user@.service.d/delegate.conf << 'EOF' -[Service] -Delegate=cpu cpuset io memory pids -EOF -sudo systemctl daemon-reload -``` - -### Arch Linux - -```bash -# cgroups v2 is default - -# Enable cgroup delegation -sudo mkdir -p /etc/systemd/system/user@.service.d/ -sudo tee /etc/systemd/system/user@.service.d/delegate.conf << 'EOF' -[Service] -Delegate=cpu cpuset io memory pids -EOF -sudo systemctl daemon-reload -``` - -## Systemd Service (Production) - -Create `/etc/systemd/system/leeward.service`: - -```ini -[Unit] -Description=Leeward Sandbox Daemon -After=network.target - -[Service] -Type=simple -ExecStart=/usr/local/bin/leeward-daemon -Restart=always -RestartSec=5 - -# Security hardening -NoNewPrivileges=true -ProtectSystem=strict -ProtectHome=true -PrivateTmp=true - -# Cgroup delegation for workers -Delegate=cpu cpuset io memory pids - -# Resource limits for the daemon itself -MemoryMax=1G -CPUQuota=200% - -[Install] -WantedBy=multi-user.target -``` - -Enable and start: -```bash -sudo systemctl enable leeward -sudo systemctl start leeward -``` - -## Docker/Podman (Alternative) - -If you can't configure cgroups on the host, run leeward inside a privileged container: - -```bash -# Docker -docker run -d --privileged --name leeward \ - -v /var/run/leeward:/var/run/leeward \ - ghcr.io/vektia/leeward:latest - -# Podman (rootless with --userns=keep-id) -podman run -d --privileged --userns=keep-id --name leeward \ - -v /var/run/leeward:/var/run/leeward \ - ghcr.io/vektia/leeward:latest -``` - -## Troubleshooting - -### "Permission denied" on cgroups - -```bash -# Check if cgroups v2 is mounted -mount | grep cgroup - -# Check delegation -cat /sys/fs/cgroup/user.slice/user-$(id -u).slice/cgroup.controllers - -# Try running with systemd-run -systemd-run --user --scope -p Delegate=yes ./target/release/leeward-daemon -``` - -### "Operation not permitted" on namespaces - -```bash -# Check if user namespaces are enabled -cat /proc/sys/kernel/unprivileged_userns_clone -# Should be 1 - -# If 0, enable: -sudo sysctl -w kernel.unprivileged_userns_clone=1 -``` - -### Landlock not available - -```bash -# Check kernel version (need >= 5.13) -uname -r - -# Check if Landlock is enabled -cat /sys/kernel/security/lsm -# Should include "landlock" -``` - -## Verification - -After setup, verify everything works: - -```bash -# Start daemon -./target/release/leeward-daemon & - -# Check status -./target/release/leeward status - -# Execute test code -./target/release/leeward exec "import os; print(os.getpid())" - -# Check isolation (should fail - no network in sandbox) -./target/release/leeward exec "import urllib.request; urllib.request.urlopen('http://google.com')" -``` diff --git a/flake.lock b/flake.lock index bafa343..c9ff4de 100644 --- a/flake.lock +++ b/flake.lock @@ -34,22 +34,6 @@ "type": "github" } }, - "nixpkgs_2": { - "locked": { - "lastModified": 1744536153, - "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, "root": { "inputs": { "flake-utils": "flake-utils", @@ -59,7 +43,9 @@ }, "rust-overlay": { "inputs": { - "nixpkgs": "nixpkgs_2" + "nixpkgs": [ + "nixpkgs" + ] }, "locked": { "lastModified": 1767322002, diff --git a/flake.nix b/flake.nix index 0750b34..31b3cff 100644 --- a/flake.nix +++ b/flake.nix @@ -1,66 +1,235 @@ { - description = "Run untrusted Python code safely with native Linux isolation"; + description = "Linux-native sandbox for running untrusted code"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; flake-utils.url = "github:numtide/flake-utils"; - rust-overlay.url = "github:oxalica/rust-overlay"; }; - outputs = inputs@{ self, nixpkgs, flake-utils, rust-overlay }: + outputs = { self, nixpkgs, rust-overlay, flake-utils }: let - nixosModules.default = import ./nix/module.nix; - in - flake-utils.lib.eachDefaultSystem (system: - let - overlays = [ (import rust-overlay) ]; - pkgs = import nixpkgs { inherit system overlays; }; - lib = import ./nix/lib.nix { inherit pkgs; }; - packages = import ./nix/packages.nix { inherit pkgs lib; }; - - # Rust toolchain - rustToolchain = pkgs.rust-bin.stable.latest.default.override { - extensions = [ "rust-src" "rust-analyzer" ]; - }; - in - { - packages = { - default = packages.leeward-all; - cli = packages.leeward-cli; - daemon = packages.leeward-daemon; - ffi = packages.leeward-ffi; - }; + supportedSystems = [ "x86_64-linux" "aarch64-linux" ]; + + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + + nixpkgsFor = forAllSystems (system: import nixpkgs { + inherit system; + overlays = [ rust-overlay.overlays.default ]; + }); + + # Read version from Cargo.toml + cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml); + version = cargoToml.workspace.package.version; + + buildLeeward = { pkgs, target ? null, static ? false }: + let + rustToolchain = if static then + pkgs.rust-bin.stable.latest.default.override { + targets = [ "x86_64-unknown-linux-musl" ]; + } + else + pkgs.rust-bin.stable.latest.default; - devShells.default = pkgs.mkShell { buildInputs = with pkgs; [ + python3 + ] ++ lib.optionals static [ + pkgs.pkgsStatic.stdenv.cc + ]; + + nativeBuildInputs = with pkgs; [ rustToolchain - cargo-watch pkg-config - libseccomp - mold - clang - llvmPackages.bintools - python3 ]; - shellHook = '' - export LIBSECCOMP_LINK_TYPE="dylib" - export LIBSECCOMP_LIB_PATH="${pkgs.libseccomp}/lib" - export PKG_CONFIG_PATH="${pkgs.libseccomp}/lib/pkgconfig" - export LEEWARD_SOCKET="$PWD/.leeward.sock" - export RUST_SRC_PATH="${rustToolchain}/lib/rustlib/src/rust/library" - - echo "πŸš€ Leeward Development Environment" - echo "" - echo "Comandos:" - echo " cargo build --release - Compila o projeto" - echo " ./target/release/leeward-daemon - Inicia o daemon" - echo " ./target/release/leeward exec 'print(1)' - Executa cΓ³digo" - echo "" - echo "Para cgroups (opcional), rode com:" - echo " sudo ./target/release/leeward-daemon" + in pkgs.rustPlatform.buildRustPackage { + pname = "leeward"; + inherit version; + + src = pkgs.lib.cleanSource ./.; + + cargoLock.lockFile = ./Cargo.lock; + + inherit buildInputs nativeBuildInputs; + + CARGO_BUILD_TARGET = if static then "x86_64-unknown-linux-musl" else null; + CARGO_BUILD_RUSTFLAGS = if static then "-C target-feature=+crt-static" else ""; + + postInstall = '' + mkdir -p $out/bin + + if [ -f target/*/release/leeward-daemon ]; then + mv target/*/release/leeward-daemon $out/bin/ + mv target/*/release/leeward $out/bin/ + else + mv target/release/leeward-daemon $out/bin/ || true + mv target/release/leeward $out/bin/ || true + fi + + ${if static then "${pkgs.binutils}/bin/strip $out/bin/*" else ""} + + mkdir -p $out/lib/systemd/{system,user} + cp contrib/leeward.system.service $out/lib/systemd/system/leeward.service + cp contrib/leeward.user.service $out/lib/systemd/user/leeward.service ''; + + meta = with pkgs.lib; { + description = "Linux-native sandbox for running untrusted code"; + homepage = "https://github.com/vektia/leeward"; + license = licenses.asl20; + platforms = [ "x86_64-linux" "aarch64-linux" ]; + mainProgram = "leeward"; + }; + }; + + buildDeb = { pkgs, leeward }: + let + arch = if pkgs.stdenv.hostPlatform.system == "x86_64-linux" then "amd64" + else if pkgs.stdenv.hostPlatform.system == "aarch64-linux" then "arm64" + else throw "Unsupported architecture"; + in pkgs.stdenv.mkDerivation { + pname = "leeward-deb"; + inherit version; + + dontUnpack = true; + dontBuild = true; + + nativeBuildInputs = [ pkgs.dpkg ]; + + installPhase = '' + mkdir -p $out + mkdir -p deb/DEBIAN deb/usr/bin deb/usr/lib/systemd/{system,user} + + cp ${leeward}/bin/* deb/usr/bin/ + cp ${leeward}/lib/systemd/system/*.service deb/usr/lib/systemd/system/ + cp ${leeward}/lib/systemd/user/*.service deb/usr/lib/systemd/user/ + cat > deb/DEBIAN/control < + Description: Linux-native sandbox for running untrusted code + Homepage: https://github.com/vektia/leeward + Section: devel + Priority: optional + Depends: python3 + EOF + + cat > deb/DEBIAN/postinst <<'EOF' + #!/bin/sh + set -e + if ! getent passwd leeward >/dev/null; then + useradd -r -s /usr/sbin/nologin -d /nonexistent leeward + fi + mkdir -p /run/leeward + chown leeward:leeward /run/leeward + if [ -d /run/systemd/system ]; then + systemctl daemon-reload + fi + EOF + chmod 755 deb/DEBIAN/postinst + + dpkg-deb --build deb $out/leeward_${version}_${arch}.deb + ''; + }; + + in { + packages = forAllSystems (system: + let + pkgs = nixpkgsFor.${system}; + leeward = buildLeeward { inherit pkgs; }; + + in { + default = leeward; + leeward-static = if system == "x86_64-linux" + then buildLeeward { inherit pkgs; static = true; } + else null; + leeward-deb = buildDeb { inherit pkgs leeward; }; + } // pkgs.lib.optionalAttrs (system == "x86_64-linux") { + leeward-x86_64 = leeward; + } // pkgs.lib.optionalAttrs (system == "aarch64-linux") { + leeward-aarch64 = leeward; + }); + + devShells = forAllSystems (system: + let + pkgs = nixpkgsFor.${system}; + rustToolchain = pkgs.rust-bin.stable.latest.default.override { + extensions = [ "rust-src" "rust-analyzer" ]; + }; + in { + default = pkgs.mkShell { + buildInputs = with pkgs; [ + rustToolchain + cargo-watch + cargo-audit + pkg-config + python3 + mold + clang + ]; + + RUST_SRC_PATH = "${rustToolchain}/lib/rustlib/src/rust/library"; + LEEWARD_SOCKET = "$PWD/.leeward.sock"; + + shellHook = ""; + }; + }); + + nixosModules.default = { config, lib, pkgs, ... }: + with lib; + let + cfg = config.services.leeward; + in { + options.services.leeward = { + enable = mkEnableOption "Leeward sandbox daemon"; + + workers = mkOption { + type = types.int; + default = 4; + description = "Number of pre-forked workers"; + }; + + package = mkOption { + type = types.package; + default = self.packages.${pkgs.system}.default; + description = "Leeward package to use"; + }; + }; + + config = mkIf cfg.enable { + systemd.services.leeward = { + description = "Leeward Sandbox Daemon"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + + serviceConfig = { + Type = "simple"; + ExecStart = "${cfg.package}/bin/leeward-daemon --workers ${toString cfg.workers}"; + Restart = "always"; + RestartSec = 5; + User = "leeward"; + Group = "leeward"; + NoNewPrivileges = true; + ProtectSystem = "strict"; + ProtectHome = true; + PrivateTmp = true; + RuntimeDirectory = "leeward"; + RuntimeDirectoryMode = "0755"; + }; + }; + + users.users.leeward = { + isSystemUser = true; + group = "leeward"; + description = "Leeward daemon user"; + }; + + users.groups.leeward = {}; + }; }; - } - ) // { inherit nixosModules; }; + }; } \ No newline at end of file diff --git a/ideia.md b/ideia.md deleted file mode 100644 index d97358a..0000000 --- a/ideia.md +++ /dev/null @@ -1,549 +0,0 @@ -════════════════════════════════════════════════════════════════════════════════ - ARQUITETURA LEEWARD v1 -════════════════════════════════════════════════════════════════════════════════ - - - VISΓƒO GERAL -──────────────────────────────────────────────────────────────────────────────── - -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ β”‚ -β”‚ CLIENT (Python/Node/Go/Rust) β”‚ -β”‚ β”‚ β”‚ -β”‚ β”‚ 1. Prepara arquivos em /data/jobs/{job_id}/input/ β”‚ -β”‚ β”‚ 2. Chama leeward via FFI β”‚ -β”‚ β”‚ 3. Recebe resultado β”‚ -β”‚ β”‚ 4. LΓͺ output de /data/jobs/{job_id}/output/ β”‚ -β”‚ β”‚ β”‚ -β”‚ β–Ό β”‚ -β”‚ libleeward.so (C FFI) β”‚ -β”‚ β”‚ β”‚ -β”‚ β”‚ - Serializa request (msgpack) β”‚ -β”‚ β”‚ - Envia via Unix socket β”‚ -β”‚ β”‚ - Deserializa response β”‚ -β”‚ β”‚ β”‚ -β””β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β”‚ Unix socket: /var/run/leeward.sock - β”‚ Protocolo: [4 bytes len][msgpack payload] - β”‚ Tamanho: ~200 bytes (sΓ³ metadata, nΓ£o dados) - β”‚ -β”Œβ”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ β–Ό β”‚ -β”‚ DAEMON (leeward-daemon) β”‚ -β”‚ β”‚ β”‚ -β”‚ β”œβ”€β”€ Server (tokio async) β”‚ -β”‚ β”‚ β”‚ β”‚ -β”‚ β”‚ β”œβ”€β”€ Aceita conexΓ΅es β”‚ -β”‚ β”‚ β”œβ”€β”€ Deserializa requests β”‚ -β”‚ β”‚ β”œβ”€β”€ Valida paths (allowed_paths check) β”‚ -β”‚ β”‚ └── Dispatch para worker pool β”‚ -β”‚ β”‚ β”‚ -β”‚ β”œβ”€β”€ Worker Pool β”‚ -β”‚ β”‚ β”‚ β”‚ -β”‚ β”‚ β”œβ”€β”€ [W1] ──┐ β”‚ -β”‚ β”‚ β”œβ”€β”€ [W2] ──┼── Processos pre-forked, isolados, Python quente β”‚ -β”‚ β”‚ β”œβ”€β”€ [W3] ─── β”‚ -β”‚ β”‚ └── [W4] β”€β”€β”˜ β”‚ -β”‚ β”‚ β”‚ -β”‚ └── Supervisor β”‚ -β”‚ β”‚ β”‚ -β”‚ β”œβ”€β”€ seccomp-notify handler β”‚ -β”‚ β”œβ”€β”€ Timeout enforcer β”‚ -β”‚ └── Metrics collector β”‚ -β”‚ β”‚ -β””β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β”‚ pipe (cΓ³digo + config, ~200 bytes) - β”‚ -β”Œβ”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ β–Ό β”‚ -β”‚ WORKER (processo isolado) β”‚ -β”‚ β”‚ β”‚ -β”‚ β”œβ”€β”€ Namespaces: user, pid, mount, net, ipc, uts β”‚ -β”‚ β”œβ”€β”€ Bind mounts: /input (ro), /output (rw), /usr (ro) β”‚ -β”‚ β”œβ”€β”€ Landlock: sΓ³ acessa paths permitidos β”‚ -β”‚ β”œβ”€β”€ Seccomp: whitelist de ~30 syscalls β”‚ -β”‚ β”œβ”€β”€ Cgroups: memory, cpu, pids limits β”‚ -β”‚ β”‚ β”‚ -β”‚ └── Python interpreter β”‚ -β”‚ β”‚ β”‚ -β”‚ β”œβ”€β”€ pandas, numpy pre-loaded β”‚ -β”‚ β”œβ”€β”€ Executa cΓ³digo do usuΓ‘rio β”‚ -β”‚ └── Retorna stdout/stderr/exit_code β”‚ -β”‚ β”‚ -β””β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β”‚ bind mount (zero-copy, mesmo inode) - β”‚ -β”Œβ”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ β–Ό β”‚ -β”‚ FILESYSTEM (host) β”‚ -β”‚ β”‚ -β”‚ /data/jobs/{job_id}/ β”‚ -β”‚ β”œβ”€β”€ input/ ← client coloca arquivos aqui β”‚ -β”‚ β”‚ β”œβ”€β”€ vendas.xlsx (2GB) worker lΓͺ via bind mount β”‚ -β”‚ β”‚ └── config.json β”‚ -β”‚ └── output/ ← worker escreve aqui β”‚ -β”‚ └── resultado.parquet client lΓͺ direto β”‚ -β”‚ β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - - - - FLUXO DE EXECUÇÃO -──────────────────────────────────────────────────────────────────────────────── - - TEMPO CLIENT FFI DAEMON WORKER -─────────────────────────────────────────────────────────────────────────────── - - 0ms β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ Prepara job β”‚ - β”‚ em /data/ β”‚ - β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - 1ms β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - └────────►│ serialize β”‚ - β”‚ request β”‚ - β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - 2ms β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - └────────►│ recv socket β”‚ - β”‚ parse msg β”‚ - β”‚ validate β”‚ - β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - 3ms β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - └────────►│ recv pipe β”‚ - β”‚ setup mountsβ”‚ - β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - 4ms β”‚ - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β–Ό - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ β”‚ - β”‚ SANDBOX EXECUTION β”‚ - β”‚ β”‚ - β”‚ /input/vendas.xlsx ◄─── bind mount ◄─── /data/jobs/abc/input/ β”‚ - β”‚ β”‚ β”‚ - β”‚ β–Ό β”‚ - β”‚ df = pd.read_excel('/input/vendas.xlsx') # zero-copy read β”‚ - β”‚ summary = df.groupby('region').sum() β”‚ - β”‚ summary.to_parquet('/output/result.parquet') β”‚ - β”‚ β”‚ β”‚ - β”‚ β–Ό β”‚ - β”‚ /output/result.parquet ──► bind mount ──► /data/jobs/abc/output/ β”‚ - β”‚ β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β”‚ (tempo de execuΓ§Γ£o variΓ‘vel) - β”‚ - Nms β”‚ - └────────────────────────────────────────┐ - β”‚ - β–Ό - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ collect β”‚ - β”‚ stdout/err β”‚ - β”‚ metrics β”‚ - β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β”‚β—„β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β” - β”‚ serialize β”‚ - β”‚ response β”‚ - β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β”‚β—„β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β” - β”‚ return β”‚ - β”‚ result β”‚ - β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β–Ό - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ LΓͺ output β”‚ - β”‚ de /data/ β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - - - - PROTOCOLO -──────────────────────────────────────────────────────────────────────────────── - -REQUEST (Client β†’ Daemon) -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ β”‚ -β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ -β”‚ β”‚ 4 bytes β”‚ msgpack payload β”‚ β”‚ -β”‚ β”‚ (length) β”‚ β”‚ β”‚ -β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ -β”‚ β”‚ -β”‚ { β”‚ -β”‚ "type": "Execute", β”‚ -β”‚ "code": "df = pd.read_excel('/input/data.xlsx')...", β”‚ -β”‚ "mounts": [ β”‚ -β”‚ {"host": "/data/jobs/abc/input", "sandbox": "/input", "ro": true}, β”‚ -β”‚ {"host": "/data/jobs/abc/output", "sandbox": "/output", "ro": false} β”‚ -β”‚ ], β”‚ -β”‚ "timeout_secs": 300, β”‚ -β”‚ "memory_limit": 4294967296 β”‚ -β”‚ } β”‚ -β”‚ β”‚ -β”‚ ~200 bytes β”‚ -β”‚ β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - - -RESPONSE (Daemon β†’ Client) -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ β”‚ -β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ -β”‚ β”‚ 4 bytes β”‚ msgpack payload β”‚ β”‚ -β”‚ β”‚ (length) β”‚ β”‚ β”‚ -β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ -β”‚ β”‚ -β”‚ { β”‚ -β”‚ "type": "Execute", β”‚ -β”‚ "success": true, β”‚ -β”‚ "result": { β”‚ -β”‚ "exit_code": 0, β”‚ -β”‚ "stdout": "Processed 1000000 rows", β”‚ -β”‚ "stderr": "", β”‚ -β”‚ "duration_ms": 45230, β”‚ -β”‚ "memory_peak": 3221225472, β”‚ -β”‚ "cpu_time_us": 44890000, β”‚ -β”‚ "timed_out": false, β”‚ -β”‚ "oom_killed": false β”‚ -β”‚ } β”‚ -β”‚ } β”‚ -β”‚ β”‚ -β”‚ ~500 bytes β”‚ -β”‚ β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - - - - ISOLAMENTO (WORKER) -──────────────────────────────────────────────────────────────────────────────── - -ORDEM DE APLICAÇÃO (crΓ­tico para seguranΓ§a): - - 1. CLONE/UNSHARE - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ clone3(CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNS | β”‚ - β”‚ CLONE_NEWNET | CLONE_NEWIPC | CLONE_NEWUTS) β”‚ - β”‚ β”‚ - β”‚ β†’ Processo em novos namespaces β”‚ - β”‚ β†’ UID 0 dentro = UID 1000 fora (mapeado) β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β–Ό - 2. FILESYSTEM SETUP - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ mount("none", "/", NULL, MS_REC | MS_PRIVATE, NULL) β”‚ - β”‚ β”‚ - β”‚ // Bind mounts β”‚ - β”‚ mount("/data/jobs/abc/input", "/sandbox/input", NULL, MS_BIND, NULL)β”‚ - β”‚ mount(NULL, "/sandbox/input", NULL, MS_REMOUNT | MS_RDONLY, NULL) β”‚ - β”‚ mount("/data/jobs/abc/output", "/sandbox/output", NULL, MS_BIND, 0) β”‚ - β”‚ β”‚ - β”‚ // Sistema read-only β”‚ - β”‚ mount("/usr", "/sandbox/usr", NULL, MS_BIND | MS_RDONLY, NULL) β”‚ - β”‚ mount("/lib", "/sandbox/lib", NULL, MS_BIND | MS_RDONLY, NULL) β”‚ - β”‚ β”‚ - β”‚ // tmpfs para temp β”‚ - β”‚ mount("tmpfs", "/sandbox/tmp", "tmpfs", 0, "size=100M") β”‚ - β”‚ β”‚ - β”‚ // pivot_root β”‚ - β”‚ pivot_root("/sandbox", "/sandbox/.old") β”‚ - β”‚ umount2("/.old", MNT_DETACH) β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β–Ό - 3. LANDLOCK - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ ruleset_fd = landlock_create_ruleset(&attr, size, 0) β”‚ - β”‚ β”‚ - β”‚ // Permite leitura β”‚ - β”‚ landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, β”‚ - β”‚ {.allowed_access = READ, .parent_fd = /input}) β”‚ - β”‚ landlock_add_rule(..., {READ, /usr}) β”‚ - β”‚ landlock_add_rule(..., {READ, /lib}) β”‚ - β”‚ β”‚ - β”‚ // Permite escrita β”‚ - β”‚ landlock_add_rule(..., {READ | WRITE, /output}) β”‚ - β”‚ landlock_add_rule(..., {READ | WRITE, /tmp}) β”‚ - β”‚ β”‚ - β”‚ landlock_restrict_self(ruleset_fd, 0) β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β–Ό - 4. CAPABILITIES DROP - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ cap_clear(caps) β”‚ - β”‚ cap_set_proc(caps) β”‚ - β”‚ β”‚ - β”‚ prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β–Ό - 5. SECCOMP (ΓΊltimo - trava interface de syscalls) - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ BPF filter (whitelist): β”‚ - β”‚ β”‚ - β”‚ ALLOW: read, write, close, fstat, lseek, mmap, mprotect, β”‚ - β”‚ munmap, brk, rt_sigaction, rt_sigprocmask, ioctl, β”‚ - β”‚ access, dup, dup2, getpid, getuid, getgid, geteuid, β”‚ - β”‚ getegid, fcntl, openat, newfstatat, exit, exit_group, β”‚ - β”‚ futex, getrandom, clock_gettime, clock_nanosleep β”‚ - β”‚ β”‚ - β”‚ DENY (with EACCES via notify): everything else β”‚ - β”‚ β”‚ - β”‚ seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, β”‚ - β”‚ &prog) β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β–Ό - 6. CGROUPS - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ // Daemon jΓ‘ criou cgroup antes do fork β”‚ - β”‚ /sys/fs/cgroup/leeward/worker-{id}/ β”‚ - β”‚ β”‚ - β”‚ memory.max = 4294967296 (4GB) β”‚ - β”‚ memory.swap.max = 0 (no swap) β”‚ - β”‚ cpu.max = 100000 100000 (100%) β”‚ - β”‚ pids.max = 64 (max processes) β”‚ - β”‚ β”‚ - β”‚ // Worker adicionado ao cgroup β”‚ - β”‚ echo $PID > /sys/fs/cgroup/leeward/worker-{id}/cgroup.procs β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β–Ό - 7. EXEC PYTHON - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ execve("/usr/bin/python3", ["python3", "-c", code], env) β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - - - - WORKER POOL LIFECYCLE -──────────────────────────────────────────────────────────────────────────────── - -STARTUP -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ β”‚ -β”‚ Daemon β”‚ -β”‚ β”‚ β”‚ -β”‚ β”œβ”€β”€β–Ί fork() ──► Worker 1 ──► setup isolation ──► Python ready (idle) β”‚ -β”‚ β”œβ”€β”€β–Ί fork() ──► Worker 2 ──► setup isolation ──► Python ready (idle) β”‚ -β”‚ β”œβ”€β”€β–Ί fork() ──► Worker 3 ──► setup isolation ──► Python ready (idle) β”‚ -β”‚ └──► fork() ──► Worker 4 ──► setup isolation ──► Python ready (idle) β”‚ -β”‚ β”‚ -β”‚ Pool: [W1:idle] [W2:idle] [W3:idle] [W4:idle] β”‚ -β”‚ β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - - -REQUEST HANDLING -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ β”‚ -β”‚ Request chega β”‚ -β”‚ β”‚ β”‚ -β”‚ β–Ό β”‚ -β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ -β”‚ β”‚ Pool: [W1:idle] [W2:idle] [W3:idle] [W4:idle] β”‚ β”‚ -β”‚ β”‚ β”‚ β”‚ β”‚ -β”‚ β”‚ └──► grab W1 β”‚ β”‚ -β”‚ β”‚ β”‚ β”‚ -β”‚ β”‚ Pool: [W1:busy] [W2:idle] [W3:idle] [W4:idle] β”‚ β”‚ -β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ -β”‚ β”‚ β”‚ -β”‚ β”œβ”€β”€β–Ί send code via pipe β”‚ -β”‚ β”‚ β”‚ -β”‚ β”‚ W1 executes... β”‚ -β”‚ β”‚ β”‚ -β”‚ ◄──► recv result via pipe β”‚ -β”‚ β”‚ β”‚ -β”‚ β–Ό β”‚ -β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ -β”‚ β”‚ W1.execution_count++ β”‚ β”‚ -β”‚ β”‚ β”‚ β”‚ -β”‚ β”‚ if execution_count >= 100: β”‚ β”‚ -β”‚ β”‚ recycle(W1) # kill + respawn β”‚ β”‚ -β”‚ β”‚ else: β”‚ β”‚ -β”‚ β”‚ W1.state = idle β”‚ β”‚ -β”‚ β”‚ β”‚ β”‚ -β”‚ β”‚ Pool: [W1:idle] [W2:idle] [W3:idle] [W4:idle] β”‚ β”‚ -β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ -β”‚ β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - - -RECYCLING -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ β”‚ -β”‚ Por que reciclar: β”‚ -β”‚ - Memory leaks no Python β”‚ -β”‚ - Estado poluΓ­do entre execuΓ§Γ΅es β”‚ -β”‚ - SeguranΓ§a (limpa qualquer state residual) β”‚ -β”‚ β”‚ -β”‚ recycle(W1): β”‚ -β”‚ β”‚ β”‚ -β”‚ β”œβ”€β”€β–Ί W1.state = recycling β”‚ -β”‚ β”œβ”€β”€β–Ί kill(W1.pid, SIGKILL) β”‚ -β”‚ β”œβ”€β”€β–Ί waitpid(W1.pid) β”‚ -β”‚ β”œβ”€β”€β–Ί destroy_cgroup(W1) β”‚ -β”‚ β”œβ”€β”€β–Ί fork() ──► new W1 β”‚ -β”‚ β”œβ”€β”€β–Ί setup isolation β”‚ -β”‚ └──► W1.state = idle, W1.execution_count = 0 β”‚ -β”‚ β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - - - - ESTRUTURA -──────────────────────────────────────────────────────────────────────────────── - -CRATES -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ β”‚ -β”‚ leeward-core β”‚ -β”‚ β”œβ”€β”€ src/ β”‚ -β”‚ β”‚ β”œβ”€β”€ lib.rs β”‚ -β”‚ β”‚ β”œβ”€β”€ config.rs # SandboxConfig β”‚ -β”‚ β”‚ β”œβ”€β”€ error.rs # LeewardError β”‚ -β”‚ β”‚ β”œβ”€β”€ result.rs # ExecutionResult β”‚ -β”‚ β”‚ β”œβ”€β”€ worker.rs # Worker struct β”‚ -β”‚ β”‚ └── isolation/ β”‚ -β”‚ β”‚ β”œβ”€β”€ mod.rs β”‚ -β”‚ β”‚ β”œβ”€β”€ namespace.rs # clone/unshare β”‚ -β”‚ β”‚ β”œβ”€β”€ mounts.rs # bind mounts, pivot_root β”‚ -β”‚ β”‚ β”œβ”€β”€ landlock.rs # filesystem restrictions β”‚ -β”‚ β”‚ β”œβ”€β”€ seccomp.rs # syscall filter β”‚ -β”‚ β”‚ └── cgroups.rs # resource limits β”‚ -β”‚ β”‚ β”‚ -β”‚ leeward-daemon β”‚ -β”‚ β”œβ”€β”€ src/ β”‚ -β”‚ β”‚ β”œβ”€β”€ main.rs # tokio server β”‚ -β”‚ β”‚ β”œβ”€β”€ config.rs # DaemonConfig β”‚ -β”‚ β”‚ β”œβ”€β”€ server.rs # socket handler β”‚ -β”‚ β”‚ β”œβ”€β”€ pool.rs # WorkerPool β”‚ -β”‚ β”‚ └── protocol.rs # msgpack Request/Response β”‚ -β”‚ β”‚ β”‚ -β”‚ leeward-ffi β”‚ -β”‚ β”œβ”€β”€ src/lib.rs # C FFI exports β”‚ -β”‚ β”œβ”€β”€ build.rs # cbindgen β”‚ -β”‚ └── cbindgen.toml β”‚ -β”‚ β”‚ β”‚ -β”‚ leeward-cli β”‚ -β”‚ └── src/main.rs # CLI tool β”‚ -β”‚ β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - - -BINDINGS -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ β”‚ -β”‚ bindings/ β”‚ -β”‚ β”œβ”€β”€ python/ β”‚ -β”‚ β”‚ β”œβ”€β”€ leeward/ β”‚ -β”‚ β”‚ β”‚ β”œβ”€β”€ __init__.py β”‚ -β”‚ β”‚ β”‚ └── client.py # ctypes wrapper β”‚ -β”‚ β”‚ └── pyproject.toml β”‚ -β”‚ β”‚ β”‚ -β”‚ β”œβ”€β”€ node/ (futuro) β”‚ -β”‚ β”‚ └── ... # ffi-napi β”‚ -β”‚ β”‚ β”‚ -β”‚ └── go/ (futuro) β”‚ -β”‚ └── ... # cgo β”‚ -β”‚ β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - - - - MΓ‰TRICAS -──────────────────────────────────────────────────────────────────────────────── - -PROMETHEUS (porta 9090) -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ β”‚ -β”‚ # Pool β”‚ -β”‚ leeward_workers_total{state="idle|busy|recycling"} β”‚ -β”‚ leeward_worker_recycles_total β”‚ -β”‚ β”‚ -β”‚ # ExecuΓ§Γ΅es β”‚ -β”‚ leeward_executions_total β”‚ -β”‚ leeward_executions_failed_total{reason="timeout|oom|error"} β”‚ -β”‚ leeward_execution_duration_seconds{quantile="0.5|0.9|0.99"} β”‚ -β”‚ β”‚ -β”‚ # Recursos β”‚ -β”‚ leeward_memory_usage_bytes{worker="1|2|3|4"} β”‚ -β”‚ leeward_cpu_time_seconds_total β”‚ -β”‚ β”‚ -β”‚ # Socket β”‚ -β”‚ leeward_connections_active β”‚ -β”‚ leeward_requests_total β”‚ -β”‚ β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - - - - PERFORMANCE -──────────────────────────────────────────────────────────────────────────────── - -LATÊNCIA BREAKDOWN (warm request, print('hello')) -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ β”‚ -β”‚ Client serialize 0.05ms β”‚ -β”‚ Socket send 0.10ms β”‚ -β”‚ Daemon recv + parse 0.10ms β”‚ -β”‚ Pool get worker 0.05ms β”‚ -β”‚ Pipe send to worker 0.10ms β”‚ -β”‚ Worker recv 0.05ms β”‚ -β”‚ Mount setup (per-job) 0.50ms β”‚ -β”‚ Python exec 1.50ms β”‚ -β”‚ Collect stdout 0.10ms β”‚ -β”‚ Pipe send result 0.10ms β”‚ -β”‚ Daemon serialize 0.05ms β”‚ -β”‚ Socket send 0.10ms β”‚ -β”‚ Client deserialize 0.05ms β”‚ -β”‚ ───────────────────────────────── β”‚ -β”‚ TOTAL ~3ms β”‚ -β”‚ β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - - -THROUGHPUT -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ β”‚ -β”‚ 4 workers Γ— 300 req/s/worker = ~1,200 req/s β”‚ -β”‚ 8 workers Γ— 300 req/s/worker = ~2,400 req/s β”‚ -β”‚ 16 workers Γ— 300 req/s/worker = ~4,800 req/s β”‚ -β”‚ β”‚ -β”‚ Bottleneck: Python execution time β”‚ -β”‚ Com pandas hot: ~50 req/s/worker (processamento real) β”‚ -β”‚ β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - - - - COMPARAÇÃO FINAL -──────────────────────────────────────────────────────────────────────────────── - -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ β”‚ E2B β”‚ Docker β”‚ Modal β”‚ WASM β”‚ leeward β”‚ -β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -β”‚ Cold start β”‚ 200ms β”‚ 400ms β”‚ 150ms β”‚ 5ms β”‚ 45ms β”‚ -β”‚ Warm request β”‚ 50ms β”‚ 80ms β”‚ 30ms β”‚ 1ms β”‚ 3ms β”‚ -β”‚ 2GB file β”‚ 10s+ β”‚ 50ms β”‚ 10s+ β”‚ N/A β”‚ ~0ms (bind mount) β”‚ -β”‚ Throughput β”‚ 500/s β”‚ 100/s β”‚ 1000/s β”‚ 5000/s β”‚ 5000/s β”‚ -β”‚ Mem/worker β”‚ 50MB β”‚ 100MB β”‚ 50MB β”‚ 10MB β”‚ 15MB β”‚ -β”‚ Native libs β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ— β”‚ βœ“ β”‚ -β”‚ Self-hosted β”‚ βœ— β”‚ βœ“ β”‚ βœ— β”‚ βœ“ β”‚ βœ“ β”‚ -β”‚ Pricing β”‚ $$$ β”‚ server β”‚ $$ β”‚ free β”‚ free β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ \ No newline at end of file diff --git a/nix/lib.nix b/nix/lib.nix deleted file mode 100644 index 398fd95..0000000 --- a/nix/lib.nix +++ /dev/null @@ -1,14 +0,0 @@ -{ pkgs }: - -{ - cargoVersion = path: - let - cargoToml = builtins.fromTOML (builtins.readFile path); - in - cargoToml.workspace.package.version or cargoToml.package.version; - - buildDeps = with pkgs; [ - pkg-config - libseccomp - ]; -} \ No newline at end of file diff --git a/nix/module.nix b/nix/module.nix deleted file mode 100644 index 6041ec3..0000000 --- a/nix/module.nix +++ /dev/null @@ -1,149 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; - -let - cfg = config.services.leeward; - - leewardLib = import ./lib.nix { inherit pkgs; }; - leewardPkgs = import ./packages.nix { inherit pkgs; lib = leewardLib; }; -in { - options.services.leeward = { - enable = mkEnableOption "leeward sandbox daemon"; - - package = mkOption { - type = types.package; - default = leewardPkgs.leeward-daemon; - description = "The leeward package to use."; - }; - - socketPath = mkOption { - type = types.str; - default = "/run/leeward/leeward.sock"; - description = "Path to the Unix socket."; - }; - - numWorkers = mkOption { - type = types.int; - default = 4; - description = "Number of worker processes in the pool."; - }; - - recycleAfter = mkOption { - type = types.int; - default = 100; - description = "Recycle workers after this many executions."; - }; - - memoryLimit = mkOption { - type = types.str; - default = "256M"; - description = "Memory limit per worker (e.g., 256M, 1G)."; - }; - - cpuQuota = mkOption { - type = types.str; - default = "100%"; - description = "CPU quota for workers (e.g., 100%, 200% for 2 cores)."; - }; - - user = mkOption { - type = types.str; - default = "leeward"; - description = "User to run the daemon as."; - }; - - group = mkOption { - type = types.str; - default = "leeward"; - description = "Group for socket access."; - }; - }; - - config = mkIf cfg.enable { - # Kernel parameters for sandbox features - boot.kernel.sysctl = { - "kernel.unprivileged_userns_clone" = 1; - }; - - systemd.services.leeward = { - description = "Leeward sandbox daemon"; - documentation = [ "https://github.com/vektia/leeward" ]; - after = [ "network.target" ]; - wantedBy = [ "multi-user.target" ]; - - environment = { - LEEWARD_SOCKET = cfg.socketPath; - LEEWARD_WORKERS = toString cfg.numWorkers; - LEEWARD_RECYCLE_AFTER = toString cfg.recycleAfter; - }; - - serviceConfig = { - Type = "simple"; - ExecStart = "${cfg.package}/bin/leeward-daemon"; - Restart = "on-failure"; - RestartSec = "5s"; - - # Run as dedicated user - User = cfg.user; - Group = cfg.group; - - # Cgroup delegation - CRITICAL for sandbox to create worker cgroups - Delegate = "cpu cpuset io memory pids"; - - # Security hardening - NoNewPrivileges = false; # Need for user namespaces - PrivateTmp = true; - ProtectSystem = "strict"; - ProtectHome = true; - ReadWritePaths = [ "/run/leeward" ]; - - # Capabilities needed for namespaces and cgroups - AmbientCapabilities = [ - "CAP_SYS_ADMIN" # For mount namespaces - "CAP_SETUID" # For user namespaces - "CAP_SETGID" # For user namespaces - "CAP_NET_ADMIN" # For network namespaces - "CAP_SYS_PTRACE" # For seccomp user notifications - ]; - CapabilityBoundingSet = [ - "CAP_SYS_ADMIN" - "CAP_SETUID" - "CAP_SETGID" - "CAP_NET_ADMIN" - "CAP_SYS_PTRACE" - ]; - - # Runtime directory - RuntimeDirectory = "leeward"; - RuntimeDirectoryMode = "0755"; - - # Resource limits for the daemon itself - MemoryMax = "2G"; - TasksMax = "256"; - - # Logging - StandardOutput = "journal"; - StandardError = "journal"; - SyslogIdentifier = "leeward-daemon"; - }; - }; - - # Create dedicated user and group - users.users.${cfg.user} = { - isSystemUser = true; - group = cfg.group; - description = "Leeward sandbox daemon user"; - }; - - users.groups.${cfg.group} = {}; - - # Ensure runtime directory permissions - systemd.tmpfiles.rules = [ - "d /run/leeward 0755 ${cfg.user} ${cfg.group} - -" - ]; - - # Add CLI to system packages - environment.systemPackages = [ leewardPkgs.leeward-cli ]; - }; -} diff --git a/nix/packages.nix b/nix/packages.nix deleted file mode 100644 index 62c4077..0000000 --- a/nix/packages.nix +++ /dev/null @@ -1,137 +0,0 @@ -{ pkgs, lib }: - -let - version = lib.cargoVersion ../Cargo.toml; - buildDeps = lib.buildDeps; - src = pkgs.lib.cleanSource ../.; - - # Python with commonly needed packages for sandboxed execution - pythonEnv = pkgs.python3.withPackages (ps: with ps; [ - # Base packages users might expect - ]); - - # Runtime dependencies that need to be available in the sandbox - runtimeDeps = [ - pythonEnv - pkgs.coreutils - pkgs.bash - ]; - - # Paths to bind-mount into sandbox (read-only) - sandboxPaths = pkgs.lib.concatMapStringsSep ":" (p: "${p}") runtimeDeps; - - rustBuild = { - inherit version src; - cargoLock.lockFile = ../Cargo.lock; - nativeBuildInputs = with pkgs; [ clang mold ] ++ buildDeps; - buildInputs = buildDeps; - RUSTFLAGS = "-C link-arg=-fuse-ld=mold"; - }; - - leeward-cli-unwrapped = pkgs.rustPlatform.buildRustPackage (rustBuild // { - pname = "leeward-cli"; - cargoBuildFlags = [ "-p" "leeward-cli" ]; - cargoTestFlags = [ "-p" "leeward-cli" ]; - }); - - leeward-daemon-unwrapped = pkgs.rustPlatform.buildRustPackage (rustBuild // { - pname = "leeward-daemon"; - cargoBuildFlags = [ "-p" "leeward-daemon" ]; - cargoTestFlags = [ "-p" "leeward-daemon" ]; - }); - - # Wrapped daemon with all runtime dependencies in PATH - leeward-daemon = pkgs.stdenv.mkDerivation { - pname = "leeward-daemon"; - inherit version; - - nativeBuildInputs = [ pkgs.makeWrapper ]; - - # No source, just wrapping - dontUnpack = true; - - installPhase = '' - runHook preInstall - - mkdir -p $out/bin $out/share/leeward - - # Create wrapper with proper environment - makeWrapper ${leeward-daemon-unwrapped}/bin/leeward-daemon $out/bin/leeward-daemon \ - --prefix PATH : "${pkgs.lib.makeBinPath runtimeDeps}" \ - --set LEEWARD_PYTHON_PATH "${pythonEnv}/bin/python3" \ - --set LEEWARD_SANDBOX_PATHS "${sandboxPaths}" - - # Symlink the unwrapped binary for debugging - ln -s ${leeward-daemon-unwrapped}/bin/leeward-daemon $out/bin/leeward-daemon-unwrapped - - runHook postInstall - ''; - - meta = with pkgs.lib; { - description = "Leeward sandbox daemon with pre-configured Python environment"; - homepage = "https://github.com/vektia/leeward"; - license = licenses.asl20; - platforms = platforms.linux; - }; - }; - - # Wrapped CLI - leeward-cli = pkgs.stdenv.mkDerivation { - pname = "leeward-cli"; - inherit version; - - nativeBuildInputs = [ pkgs.makeWrapper ]; - dontUnpack = true; - - installPhase = '' - runHook preInstall - - mkdir -p $out/bin - - makeWrapper ${leeward-cli-unwrapped}/bin/leeward $out/bin/leeward \ - --prefix PATH : "${pkgs.lib.makeBinPath runtimeDeps}" - - runHook postInstall - ''; - - meta = with pkgs.lib; { - description = "Leeward CLI"; - homepage = "https://github.com/vektia/leeward"; - license = licenses.asl20; - platforms = platforms.linux; - }; - }; - - leeward-ffi = pkgs.rustPlatform.buildRustPackage (rustBuild // { - pname = "leeward-ffi"; - cargoBuildFlags = [ "-p" "leeward-ffi" ]; - cargoTestFlags = [ "-p" "leeward-ffi" ]; - nativeBuildInputs = with pkgs; [ clang mold cbindgen ] ++ buildDeps; - - postInstall = '' - mkdir -p $out/lib $out/include - cp target/release/libleeward.so $out/lib/ 2>/dev/null || true - cp target/release/libleeward.a $out/lib/ 2>/dev/null || true - if [ -f include/leeward.h ]; then - cp include/leeward.h $out/include/ - fi - ''; - }); - -in -{ - inherit leeward-cli leeward-daemon leeward-ffi; - inherit leeward-cli-unwrapped leeward-daemon-unwrapped; - inherit pythonEnv runtimeDeps; - - leeward-all = pkgs.symlinkJoin { - name = "leeward-${version}"; - paths = [ leeward-cli leeward-daemon leeward-ffi ]; - meta = with pkgs.lib; { - description = "Complete leeward sandbox suite"; - homepage = "https://github.com/vektia/leeward"; - license = licenses.asl20; - platforms = platforms.linux; - }; - }; -} \ No newline at end of file