use std::fmt::Write as _;
use std::path::Path;
pub const NAME: &str = "moat-proxy.service";
pub struct Spec<'a> {
pub binary: &'a Path,
pub moat_home: &'a Path,
}
pub fn render(spec: &Spec<'_>) -> String {
let mut out = String::new();
out.push_str("# OpenMoat egress proxy, installed by `moat proxy install`.\n");
out.push_str("# Managed by moat; do not edit by hand.\n");
out.push_str("\n[Unit]\n");
out.push_str("Description=OpenMoat default-deny egress proxy\n");
out.push_str("After=default.target\n");
out.push_str("\n[Service]\n");
out.push_str("Type=simple\n");
let _ = writeln!(
out,
"ExecStart={} proxy",
escape(&spec.binary.to_string_lossy())
);
let _ = writeln!(
out,
"Environment=MOAT_HOME={}",
escape(&spec.moat_home.to_string_lossy())
);
out.push_str("Restart=on-failure\n");
out.push_str("RestartSec=2\n");
out.push_str("StartLimitBurst=5\n");
out.push_str("StartLimitIntervalSec=30\n");
out.push_str("NoNewPrivileges=yes\n");
out.push_str("PrivateTmp=yes\n");
out.push_str("ProtectSystem=full\n");
out.push_str("\n[Install]\n");
out.push_str("WantedBy=default.target\n");
out
}
fn escape(text: &str) -> String {
let mut out = String::with_capacity(text.len());
for ch in text.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'\t' => out.push_str("\\t"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
_ => out.push(ch),
}
}
out
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
#[test]
fn unit_names_the_binary_and_moat_home_and_restarts_only_on_failure() {
assert_eq!(NAME, "moat-proxy.service");
let text = render(&Spec {
binary: &PathBuf::from("/home/u/.cargo/bin/moat"),
moat_home: &PathBuf::from("/home/u/.moat"),
});
assert!(text.contains("Description=OpenMoat"));
assert!(text.contains("ExecStart=/home/u/.cargo/bin/moat proxy"));
assert!(text.contains("Environment=MOAT_HOME=/home/u/.moat"));
assert!(text.contains("Restart=on-failure"));
assert!(!text.contains("Restart=always"));
assert!(text.contains("WantedBy=default.target"));
assert!(text.contains("NoNewPrivileges=yes"));
assert!(text.ends_with('\n'));
}
#[test]
fn render_is_deterministic() {
let spec = Spec {
binary: &PathBuf::from("/opt/moat"),
moat_home: &PathBuf::from("/x/.moat"),
};
assert_eq!(render(&spec), render(&spec));
}
#[test]
fn backslashes_tabs_and_newlines_in_a_path_are_escaped() {
let home = PathBuf::from("/home/weird\t\\path");
let text = render(&Spec {
binary: &PathBuf::from("/bin/moat"),
moat_home: &home,
});
assert!(
text.contains("Environment=MOAT_HOME=/home/weird\\t\\\\path"),
"{text}"
);
}
}