use crate::error::AybError;
use std::env::current_exe;
use std::path::{Path, PathBuf};
use crate::hosted_db::paths::pathbuf_to_parent;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IsolationStatus {
Full,
FilesystemAndRlimitOnly,
RlimitOnly,
None,
}
pub fn detect_isolation_status() -> IsolationStatus {
#[cfg(not(target_os = "linux"))]
{
IsolationStatus::None
}
#[cfg(target_os = "linux")]
{
match kernel_version() {
Some((major, minor)) if (major, minor) >= (6, 7) => IsolationStatus::Full,
Some((major, minor)) if (major, minor) >= (5, 13) => {
IsolationStatus::FilesystemAndRlimitOnly
}
_ => IsolationStatus::RlimitOnly,
}
}
}
#[cfg(target_os = "linux")]
fn kernel_version() -> Option<(u32, u32)> {
let release = std::fs::read_to_string("/proc/sys/kernel/osrelease").ok()?;
let mut parts = release.trim().split(['.', '-']);
let major = parts.next()?.parse().ok()?;
let minor = parts.next()?.parse().ok()?;
Some((major, minor))
}
pub fn print_isolation_status(status: IsolationStatus) {
match status {
IsolationStatus::Full => {
println!(
"Isolation: Landlock (filesystem + network) + setrlimit active on query daemons."
);
}
IsolationStatus::FilesystemAndRlimitOnly => {
print_warning_banner(&[
"Landlock network isolation unavailable (requires Linux 6.7+).",
"Filesystem isolation and resource limits ARE active.",
"Network access from query daemons is NOT restricted.",
]);
}
IsolationStatus::RlimitOnly => {
print_warning_banner(&[
"Landlock unavailable on this kernel (requires Linux 5.13+).",
"Only setrlimit resource limits are enforced.",
"Filesystem and network access are NOT restricted.",
]);
}
IsolationStatus::None => {
print_warning_banner(&[
"Landlock is unavailable on this non-Linux platform.",
"No filesystem, network, or resource limits are enforced.",
]);
}
}
}
fn print_warning_banner(details: &[&str]) {
eprintln!("======================================================================");
eprintln!("WARNING: ayb query daemons are running with degraded isolation.");
for line in details {
eprintln!("{line}");
}
eprintln!("Do NOT run multi-tenant workloads in this configuration.");
eprintln!("See https://github.com/marcua/ayb#isolation for details.");
eprintln!("======================================================================");
}
#[cfg_attr(not(target_os = "linux"), allow(unused_variables))]
pub fn apply_sandbox(db_path: &Path) -> Result<(), AybError> {
#[cfg(target_os = "linux")]
{
apply_landlock_restrictions(db_path)?;
apply_resource_limits()?;
}
Ok(())
}
#[cfg(target_os = "linux")]
fn apply_landlock_restrictions(db_path: &Path) -> Result<(), AybError> {
use landlock::{
path_beneath_rules, Access, AccessFs, AccessNet, Ruleset, RulesetAttr, RulesetCreatedAttr,
ABI,
};
let abi = ABI::V5;
let access_all = AccessFs::from_all(abi);
let access_read = AccessFs::from_read(abi);
let mut ruleset =
Ruleset::default()
.handle_access(access_all)
.map_err(|e| AybError::Other {
message: format!("Landlock: failed to handle filesystem access: {e}"),
})?;
let access_net = AccessNet::from_all(abi);
if !access_net.is_empty() {
ruleset = ruleset
.handle_access(access_net)
.map_err(|e| AybError::Other {
message: format!("Landlock: failed to handle network access: {e}"),
})?;
}
let mut ruleset_created = ruleset.create().map_err(|e| AybError::Other {
message: format!("Landlock: failed to create ruleset: {e}"),
})?;
let read_only_paths: Vec<&str> = vec!["/lib", "/lib64", "/usr"];
let existing_read_only: Vec<&str> = read_only_paths
.into_iter()
.filter(|p| Path::new(p).exists())
.collect();
if !existing_read_only.is_empty() {
ruleset_created = ruleset_created
.add_rules(path_beneath_rules(existing_read_only, access_read))
.map_err(|e| AybError::Other {
message: format!("Landlock: failed to add read-only rules: {e}"),
})?;
}
let db_dir = db_path.parent().ok_or(AybError::Other {
message: format!(
"Cannot determine parent directory of database: {}",
db_path.display()
),
})?;
ruleset_created = ruleset_created
.add_rules(path_beneath_rules(&[db_dir], access_all))
.map_err(|e| AybError::Other {
message: format!("Landlock: failed to add database directory rule: {e}"),
})?;
let _ = ruleset_created
.restrict_self()
.map_err(|e| AybError::Other {
message: format!("Landlock: failed to restrict self: {e}"),
})?;
Ok(())
}
#[cfg(target_os = "linux")]
fn apply_resource_limits() -> Result<(), AybError> {
set_rlimit(libc::RLIMIT_AS, 64 * 1024 * 1024)?; set_rlimit(libc::RLIMIT_FSIZE, 75 * 1024 * 1024)?; set_rlimit(libc::RLIMIT_NOFILE, 10)?; Ok(())
}
#[cfg(target_os = "linux")]
fn set_rlimit(resource: libc::__rlimit_resource_t, limit: u64) -> Result<(), AybError> {
let rlim = libc::rlimit {
rlim_cur: limit,
rlim_max: limit,
};
let ret = unsafe { libc::setrlimit(resource, &rlim) };
if ret != 0 {
return Err(AybError::Other {
message: format!(
"Failed to set resource limit {}: {}",
resource,
std::io::Error::last_os_error()
),
});
}
Ok(())
}
pub fn build_daemon_command(db_path: &PathBuf) -> Result<tokio::process::Command, AybError> {
let ayb_path = current_exe()?;
let query_daemon_path = pathbuf_to_parent(&ayb_path)?.join("ayb_query_daemon");
let mut cmd = tokio::process::Command::new(&query_daemon_path);
cmd.arg(db_path);
Ok(cmd)
}