use std::ffi::CString;
use std::io;
use caps::{CapSet, Capability, CapsHashSet};
use nix::unistd::{setgid, setuid, Gid, Uid};
#[derive(Debug)]
pub enum SandboxError {
PrivilegeDrop(String),
CapabilityDrop(String),
NoNewPrivs(String),
UserNotFound(String),
}
impl std::fmt::Display for SandboxError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::PrivilegeDrop(msg) => write!(f, "Failed to drop privileges: {}", msg),
Self::CapabilityDrop(msg) => write!(f, "Failed to drop capabilities: {}", msg),
Self::NoNewPrivs(msg) => write!(f, "Failed to set no_new_privs: {}", msg),
Self::UserNotFound(name) => write!(f, "User/group not found: {}", name),
}
}
}
impl std::error::Error for SandboxError {}
#[derive(Debug, Clone)]
pub struct SandboxConfig {
pub user: Option<String>,
pub group: Option<String>,
pub drop_capabilities: bool,
pub allow_net_bind: bool,
}
impl Default for SandboxConfig {
fn default() -> Self {
Self {
user: Some("nobody".to_string()),
group: Some("nogroup".to_string()),
drop_capabilities: true,
allow_net_bind: false,
}
}
}
pub fn apply_sandbox(config: &SandboxConfig) -> Result<(), SandboxError> {
if let Some(ref group) = config.group {
drop_to_group(group)?;
}
if let Some(ref user) = config.user {
drop_to_user(user)?;
}
if config.drop_capabilities {
drop_capabilities(config.allow_net_bind)?;
}
set_no_new_privs()?;
Ok(())
}
#[allow(unsafe_code)]
fn drop_to_user(user: &str) -> Result<(), SandboxError> {
let uid = if let Ok(uid) = user.parse::<u32>() {
Uid::from_raw(uid)
} else {
let c_user = CString::new(user).map_err(|e| SandboxError::UserNotFound(e.to_string()))?;
let pwd = unsafe { nix::libc::getpwnam(c_user.as_ptr()) };
if pwd.is_null() {
return Err(SandboxError::UserNotFound(user.to_string()));
}
Uid::from_raw(unsafe { (*pwd).pw_uid })
};
setuid(uid).map_err(|e| SandboxError::PrivilegeDrop(format!("setuid failed: {}", e)))
}
#[allow(unsafe_code)]
fn drop_to_group(group: &str) -> Result<(), SandboxError> {
let gid = if let Ok(gid) = group.parse::<u32>() {
Gid::from_raw(gid)
} else {
let c_group = CString::new(group).map_err(|e| SandboxError::UserNotFound(e.to_string()))?;
let grp = unsafe { nix::libc::getgrnam(c_group.as_ptr()) };
if grp.is_null() {
return Err(SandboxError::UserNotFound(group.to_string()));
}
Gid::from_raw(unsafe { (*grp).gr_gid })
};
setgid(gid).map_err(|e| SandboxError::PrivilegeDrop(format!("setgid failed: {}", e)))
}
fn drop_capabilities(allow_net_bind: bool) -> Result<(), SandboxError> {
caps::clear(None, CapSet::Effective)
.map_err(|e| SandboxError::CapabilityDrop(format!("clear effective: {}", e)))?;
caps::clear(None, CapSet::Permitted)
.map_err(|e| SandboxError::CapabilityDrop(format!("clear permitted: {}", e)))?;
caps::clear(None, CapSet::Inheritable)
.map_err(|e| SandboxError::CapabilityDrop(format!("clear inheritable: {}", e)))?;
if allow_net_bind {
let mut caps = CapsHashSet::new();
caps.insert(Capability::CAP_NET_BIND_SERVICE);
caps::set(None, CapSet::Effective, &caps)
.map_err(|e| SandboxError::CapabilityDrop(format!("set net_bind effective: {}", e)))?;
caps::set(None, CapSet::Permitted, &caps)
.map_err(|e| SandboxError::CapabilityDrop(format!("set net_bind permitted: {}", e)))?;
}
Ok(())
}
#[allow(unsafe_code)]
fn set_no_new_privs() -> Result<(), SandboxError> {
let result = unsafe { nix::libc::prctl(nix::libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) };
if result != 0 {
return Err(SandboxError::NoNewPrivs(
io::Error::last_os_error().to_string(),
));
}
Ok(())
}
pub fn needs_sandbox() -> bool {
let effective = match caps::read(None, CapSet::Effective) {
Ok(caps) => caps,
Err(_) => return false, };
if effective.is_empty() {
return false;
}
if effective.len() == 1 && effective.contains(&Capability::CAP_NET_BIND_SERVICE) {
return false;
}
true
}
pub fn is_root() -> bool {
nix::unistd::geteuid().is_root()
}
pub fn current_uid() -> u32 {
nix::unistd::getuid().as_raw()
}
pub fn current_gid() -> u32 {
nix::unistd::getgid().as_raw()
}
#[allow(unsafe_code)]
pub fn is_no_new_privs_set() -> bool {
let result = unsafe { nix::libc::prctl(nix::libc::PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0) };
result == 1
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct SandboxStatus {
pub uid: u32,
pub gid: u32,
pub is_root: bool,
pub no_new_privs: bool,
pub capabilities: Vec<String>,
}
pub fn get_status() -> SandboxStatus {
let caps = caps::read(None, CapSet::Effective)
.map(|c| c.iter().map(|cap| format!("{:?}", cap)).collect())
.unwrap_or_default();
SandboxStatus {
uid: current_uid(),
gid: current_gid(),
is_root: is_root(),
no_new_privs: is_no_new_privs_set(),
capabilities: caps,
}
}
#[cfg(test)]
#[path = "tests/sandbox_tests.rs"]
mod tests;