use openmoat_core::ir::{Access, Checker, Effect, Enforcement};
use openmoat_core::{AtomicAction, Kind};
use serde::{Deserialize, Serialize};
use super::patterns::{Spot, literal_tree, push_unique, split, spot};
use super::{Grants, Report};
const PLATFORM_READ: [&str; 6] = [
"/dev/null",
"/dev/zero",
"/dev/random",
"/dev/urandom",
"/dev/tty",
"/dev/pts",
];
const PLATFORM_WRITE: [&str; 4] = ["/dev/null", "/dev/zero", "/dev/tty", "/dev/pts"];
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Rules {
pub read: Vec<String>,
pub write: Vec<String>,
pub connect_port: Option<u16>,
}
#[derive(Debug, Clone)]
pub struct Generated {
pub rules: Rules,
pub report: Report,
}
pub fn generate(ir: &Enforcement, grants: &Grants) -> anyhow::Result<Generated> {
let checker = ir.checker()?;
let mut g = Generated {
rules: Rules {
connect_port: grants.proxy_port,
..Rules::default()
},
report: Report::default(),
};
let own = |list: &[&str]| list.iter().map(|p| (*p).to_owned()).collect::<Vec<_>>();
let (read, write) = (Kind::FsRead, Kind::FsWrite);
g.grant(
read,
"landlock.platform",
own(&PLATFORM_READ),
"every process reads these",
);
g.grant(
write,
"landlock.platform",
own(&PLATFORM_WRITE),
"the devices every process writes",
);
let program = grants.program.iter().cloned().collect();
g.grant(
read,
"landlock.program",
program,
"the program `moat run` starts",
);
g.allows(&ir.fs.read, read, &checker);
for allowance in &ir.allowances {
let roots = allowance.patterns.iter().filter_map(|p| literal_tree(p));
for root in roots.map(str::to_owned).collect::<Vec<_>>() {
g.add(read, &allowance.rule, root, &checker);
}
g.report.allowances.push(allowance.clone());
}
g.allows(&ir.fs.write, write, &checker);
let tmp = grants.tmpdir.iter().cloned().collect();
g.grant(
write,
"landlock.tmpdir",
tmp,
"commands may read and write the temp directory",
);
let message = "`moat run --write`: commands may read and write these";
g.grant(write, "landlock.write", grants.writes.clone(), message);
g.unenforced(&ir.fs.read, read);
g.unenforced(&ir.fs.write, write);
if grants.proxy_port.is_some() {
let message = "Landlock allows TCP connections by port only: the proxy's port is \
reachable on any host";
g.report
.allowance(Kind::Net, "landlock.tcp-port", Vec::new(), message);
}
g.report.proxy_only(&ir.egress.net);
Ok(g)
}
impl Generated {
fn grant(&mut self, kind: Kind, id: &str, paths: Vec<String>, why: &str) {
if paths.is_empty() {
return;
}
for path in &paths {
push_unique(self.list(kind), path.clone());
if kind == Kind::FsWrite {
push_unique(&mut self.rules.read, path.clone());
}
}
self.report.allowance(kind, id, paths, why);
}
fn list(&mut self, kind: Kind) -> &mut Vec<String> {
match kind {
Kind::FsWrite => &mut self.rules.write,
_ => &mut self.rules.read,
}
}
fn allows(&mut self, access: &Access, kind: Kind, checker: &Checker) {
if access.default == Effect::Allow {
self.add(kind, "default", "/".to_owned(), checker);
}
for rule in &access.allow {
for pattern in split(&rule.patterns).0 {
match literal_tree(pattern) {
Some(tree) => self.add(kind, &rule.id, tree.to_owned(), checker),
None => self.report.loss(
kind,
&rule.id,
format!("`{pattern}` cannot be granted as a path, so it stays denied"),
),
}
}
}
}
fn add(&mut self, kind: Kind, rule: &str, path: String, checker: &Checker) {
let atom = match kind {
Kind::FsWrite => AtomicAction::FsWrite { path: path.clone() },
_ => AtomicAction::FsRead { path: path.clone() },
};
if checker.check_os(&atom) == Some(Effect::Deny) {
let message = format!("`{path}` is inside a denied path, so it is not granted");
self.report.loss(kind, rule, message);
} else {
push_unique(self.list(kind), path);
}
}
fn unenforced(&mut self, access: &Access, kind: Kind) {
let granted = self.list(kind).clone();
let inside = |pattern: &str| {
let base = pattern
.split(['*', '?', '[', '{'])
.next()
.unwrap_or(pattern);
matches!(spot(pattern), Spot::Anywhere(_))
|| granted.iter().any(|g| {
base == g || base.starts_with(&format!("{}/", g.trim_end_matches('/')))
})
};
let excluded = access.allow.iter().flat_map(|r| split(&r.patterns).1);
let denied = access.deny.iter().flat_map(|r| split(&r.patterns).0);
let mut patterns = Vec::new();
for pattern in denied.chain(excluded).filter(|p| inside(p)) {
push_unique(&mut patterns, pattern.to_owned());
}
if patterns.is_empty() {
return;
}
let message = "Landlock cannot deny inside a granted directory, so these stay open where \
a grant covers them; the hook still decides them";
self.report
.allowance(kind, "landlock.inside-grants", patterns, message);
}
}
#[cfg(test)]
mod tests;