#[derive(Debug, Clone, Default)]
pub struct BackendCaps {
pub enforce_fs: bool,
pub enforce_net: bool,
pub enforce_ioctl: bool,
pub enforce_scopes: bool,
pub audit_only: bool,
pub abi_version: u32,
}
#[derive(Debug, Clone)]
pub enum BackendType {
Landlock,
NoopAudit, #[allow(dead_code)]
Bpf, }
impl BackendType {
pub fn name(&self) -> &'static str {
match self {
BackendType::Landlock => "Landlock",
BackendType::NoopAudit => "NoopAudit",
BackendType::Bpf => "BPF-LSM",
}
}
}
pub fn detect_backend() -> (BackendType, BackendCaps) {
let (supported, abi) = landlock_impl::probe_abi();
if supported {
(
BackendType::Landlock,
BackendCaps {
enforce_fs: abi >= 1,
enforce_net: abi >= 4,
enforce_ioctl: abi >= 5,
enforce_scopes: abi >= 6,
audit_only: false,
abi_version: abi,
},
)
} else {
(
BackendType::NoopAudit,
BackendCaps {
enforce_fs: false,
enforce_net: false,
enforce_ioctl: false,
enforce_scopes: false,
audit_only: true,
abi_version: 0,
},
)
}
}
pub struct LandlockEnforcer {
#[cfg(target_os = "linux")]
ruleset: Option<landlock_impl::RulesetHandle>,
}
impl LandlockEnforcer {
pub fn enforce(&mut self) -> std::io::Result<()> {
#[cfg(target_os = "linux")]
if let Some(ruleset) = self.ruleset.take() {
return landlock_impl::enforce_fork_safe(ruleset);
}
Ok(())
}
}
pub fn prepare_landlock(
policy: &crate::policy::Policy,
scoped_tmp: &std::path::Path,
net_allow_ports: Option<&[u16]>,
) -> anyhow::Result<LandlockEnforcer> {
#[cfg(target_os = "linux")]
{
let ruleset = landlock_impl::create_ruleset(policy, scoped_tmp, net_allow_ports)?;
Ok(LandlockEnforcer {
ruleset: Some(ruleset),
})
}
#[cfg(not(target_os = "linux"))]
{
let _ = policy;
let _ = scoped_tmp;
let _ = net_allow_ports;
Ok(LandlockEnforcer {})
}
}
#[cfg(target_os = "linux")]
mod landlock_impl {
use landlock::{
Access, AccessFs, AccessNet, BitFlags, CompatLevel, Compatible, NetPort, PathBeneath,
PathFd, Ruleset, RulesetAttr, RulesetCreated, RulesetCreatedAttr, RulesetStatus, ABI,
};
use std::path::Path;
const SYSTEM_READ_FILES: &[&str] = &[
"/etc/hosts",
"/etc/resolv.conf",
"/etc/localtime",
"/etc/timezone",
"/etc/ld.so.cache",
];
const SYSTEM_RUNTIME_DIRS: &[&str] = &["/usr", "/lib", "/lib64", "/bin", "/sbin"];
pub(super) fn probe_abi() -> (bool, u32) {
let mut max_abi;
if Ruleset::default()
.handle_access(AccessFs::from_all(ABI::V1))
.and_then(|r| r.create())
.is_ok()
{
max_abi = 1;
} else {
return (false, 0);
}
if Ruleset::default()
.handle_access(AccessFs::from_all(ABI::V2))
.and_then(|r| r.create())
.is_ok()
{
max_abi = 2;
} else {
return (true, max_abi);
}
if Ruleset::default()
.handle_access(AccessFs::from_all(ABI::V3))
.and_then(|r| r.create())
.is_ok()
{
max_abi = 3;
} else {
return (true, max_abi);
}
if Ruleset::default()
.handle_access(AccessFs::from_all(ABI::V4))
.and_then(|r| r.create())
.is_ok()
{
max_abi = 4;
}
(true, max_abi)
}
pub(super) type RulesetHandle = RulesetCreated;
pub(super) fn enforce_fork_safe(ruleset: RulesetHandle) -> std::io::Result<()> {
match ruleset.restrict_self() {
Ok(status) => {
if status.ruleset == RulesetStatus::NotEnforced {
return Err(std::io::Error::from_raw_os_error(libc::ENOTSUP));
}
Ok(())
}
Err(_e) => Err(std::io::Error::from_raw_os_error(libc::EPERM)),
}
}
pub(super) fn create_ruleset(
policy: &crate::policy::Policy,
scoped_tmp: &Path,
net_allow_ports: Option<&[u16]>,
) -> anyhow::Result<RulesetCreated> {
let (_, abi_level) = probe_abi();
let abi = match abi_level {
1 => ABI::V1,
2 => ABI::V2,
3 => ABI::V3,
_ => ABI::V4, };
let mut ruleset = Ruleset::default();
ruleset = ruleset.handle_access(AccessFs::from_all(abi))?;
if net_allow_ports.is_some() {
ruleset = ruleset
.set_compatibility(CompatLevel::HardRequirement)
.handle_access(AccessNet::ConnectTcp)?
.set_compatibility(CompatLevel::BestEffort);
}
let mut ruleset = ruleset.create()?;
if let Some(ports) = net_allow_ports {
for &port in ports {
ruleset = ruleset.add_rule(NetPort::new(port, AccessNet::ConnectTcp))?;
}
}
if let Ok(cwd) = std::env::current_dir() {
ruleset = add_path(
ruleset,
cwd.to_string_lossy().as_ref(),
AccessFs::from_read(abi) | AccessFs::Execute,
)?;
}
ruleset = add_path(
ruleset,
scoped_tmp.to_string_lossy().as_ref(),
AccessFs::from_all(abi),
)?;
for path in SYSTEM_READ_FILES {
ruleset = add_path(ruleset, path, AccessFs::from_read(abi))?;
}
let rx = AccessFs::from_read(abi) | AccessFs::Execute;
for path in SYSTEM_RUNTIME_DIRS {
ruleset = add_path(ruleset, path, rx)?;
}
for path in &policy.fs.allow {
let expanded = expand_path(path);
ruleset = add_path(ruleset, &expanded, AccessFs::from_all(abi))?;
}
Ok(ruleset)
}
fn add_path(
ruleset: RulesetCreated,
path: &str,
access: BitFlags<AccessFs>,
) -> anyhow::Result<RulesetCreated> {
if Path::new(path).exists() {
match PathFd::new(path) {
Ok(fd) => {
return Ok(ruleset.add_rule(PathBeneath::new(fd, access))?);
}
Err(e) => {
eprintln!("WARN: Landlock failed to open path '{}': {}", path, e);
}
}
}
Ok(ruleset)
}
fn expand_path(path: &str) -> String {
let mut expanded = path.to_string();
if expanded.starts_with("~/") {
if let Ok(home) = std::env::var("HOME") {
expanded = expanded.replacen("~", &home, 1);
}
}
if expanded.contains("${HOME}") {
if let Ok(home) = std::env::var("HOME") {
expanded = expanded.replace("${HOME}", &home);
}
}
if expanded.contains("${CWD}") {
if let Ok(cwd) = std::env::current_dir() {
expanded = expanded.replace("${CWD}", &cwd.to_string_lossy());
}
}
if expanded.contains("${USER}") {
if let Ok(user) = std::env::var("USER") {
expanded = expanded.replace("${USER}", &user);
}
}
expanded
}
}
#[cfg(not(target_os = "linux"))]
mod landlock_impl {
pub(super) fn probe_abi() -> (bool, u32) {
(false, 0)
}
#[allow(dead_code)]
pub(super) fn create_ruleset(
_policy: &crate::policy::Policy,
_scoped_tmp: &std::path::Path,
) -> anyhow::Result<()> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_backend_fallback() {
let (backend, caps) = detect_backend();
#[cfg(not(target_os = "linux"))]
{
assert!(matches!(backend, BackendType::NoopAudit));
assert!(caps.audit_only);
assert!(!caps.enforce_fs);
assert!(!caps.enforce_net);
}
let _ = (backend, caps);
}
}