use std::path::PathBuf;
use crate::orchestration::{CapabilityPolicy, ProcessSandboxPolicy};
pub const LOOPBACK_UNAVAILABLE_CATEGORY: &str = "sandbox.loopback_unavailable";
pub const LOOPBACK_GRANT_FIELD: &str = "allow_tcp_loopback";
pub const NETNS_LAUNCHER_FIELD: &str = "process.netns_launcher_path";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LoopbackUnavailableReason {
NoLauncherConfigured,
LauncherPathNotAbsolute,
LauncherMissing,
UnsupportedPlatform,
}
impl LoopbackUnavailableReason {
pub fn as_str(self) -> &'static str {
match self {
Self::NoLauncherConfigured => "no_launcher_configured",
Self::LauncherPathNotAbsolute => "launcher_path_not_absolute",
Self::LauncherMissing => "launcher_missing",
Self::UnsupportedPlatform => "unsupported_platform",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LoopbackUnavailable {
pub reason: LoopbackUnavailableReason,
pub launcher: Option<String>,
}
impl LoopbackUnavailable {
fn is_droppable(&self) -> bool {
self.reason != LoopbackUnavailableReason::UnsupportedPlatform
}
pub fn notice(&self) -> String {
let need = format!(
"Linux gives a confined command loopback networking only inside a private network \
namespace, which needs the namespace launcher: an executable copy of the Harn \
runtime, at an absolute path set as `{NETNS_LAUNCHER_FIELD}` in the host's sandbox \
policy"
);
let cause = match (self.reason, self.launcher.as_deref()) {
(LoopbackUnavailableReason::NoLauncherConfigured, _) => {
"No launcher is configured.".to_string()
}
(LoopbackUnavailableReason::LauncherPathNotAbsolute, Some(path)) => {
format!("The configured launcher path `{path}` is not absolute.")
}
(LoopbackUnavailableReason::LauncherMissing, Some(path)) => {
format!("The configured launcher `{path}` is not an existing executable file on this host.")
}
(LoopbackUnavailableReason::UnsupportedPlatform, _) => {
return format!(
"Loopback networking is unavailable on this host: this platform has no \
mechanism that confines a command to loopback. The `{LOOPBACK_GRANT_FIELD}` \
grant was dropped; everything else stays confined, and commands that need \
loopback will fail."
);
}
(_, None) => String::new(),
};
format!(
"Loopback networking is unavailable on this host. The `{LOOPBACK_GRANT_FIELD}` grant \
was dropped; everything else stays confined, and commands that need loopback will \
fail. {need}. {cause}"
)
.trim_end()
.to_string()
}
pub fn metadata(&self) -> std::collections::BTreeMap<String, serde_json::Value> {
let mut metadata = std::collections::BTreeMap::from([
("grant".to_string(), LOOPBACK_GRANT_FIELD.into()),
("reason".to_string(), self.reason.as_str().into()),
("launcher_field".to_string(), NETNS_LAUNCHER_FIELD.into()),
]);
if let Some(launcher) = &self.launcher {
metadata.insert("launcher".to_string(), launcher.clone().into());
}
metadata
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LoopbackSupport {
Supported {
launcher: Option<PathBuf>,
},
Unavailable(LoopbackUnavailable),
}
impl LoopbackSupport {
pub fn is_supported(&self) -> bool {
matches!(self, Self::Supported { .. })
}
}
pub fn loopback_support(process: &ProcessSandboxPolicy) -> LoopbackSupport {
#[cfg(target_os = "linux")]
{
let unavailable = |reason, launcher: Option<String>| {
LoopbackSupport::Unavailable(LoopbackUnavailable { reason, launcher })
};
let Some(path) = process.netns_launcher_path.as_ref() else {
return unavailable(LoopbackUnavailableReason::NoLauncherConfigured, None);
};
let launcher = PathBuf::from(path);
if !launcher.is_absolute() {
return unavailable(
LoopbackUnavailableReason::LauncherPathNotAbsolute,
Some(path.clone()),
);
}
if !is_executable_file(&launcher) {
return unavailable(
LoopbackUnavailableReason::LauncherMissing,
Some(path.clone()),
);
}
LoopbackSupport::Supported {
launcher: Some(launcher),
}
}
#[cfg(target_os = "macos")]
{
let _ = process;
LoopbackSupport::Supported { launcher: None }
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
LoopbackSupport::Unavailable(LoopbackUnavailable {
reason: LoopbackUnavailableReason::UnsupportedPlatform,
launcher: process.netns_launcher_path.clone(),
})
}
}
#[cfg(target_os = "linux")]
fn is_executable_file(path: &std::path::Path) -> bool {
use std::os::unix::fs::PermissionsExt;
std::fs::metadata(path)
.is_ok_and(|meta| meta.is_file() && meta.permissions().mode() & 0o111 != 0)
}
pub fn withhold_unsupported_loopback(
process: &mut ProcessSandboxPolicy,
) -> Option<LoopbackUnavailable> {
if !process.allow_tcp_loopback {
return None;
}
match loopback_support(process) {
LoopbackSupport::Supported { .. } => None,
LoopbackSupport::Unavailable(unavailable) if !unavailable.is_droppable() => None,
LoopbackSupport::Unavailable(unavailable) => {
process.allow_tcp_loopback = false;
Some(unavailable)
}
}
}
static NOTICE_EMITTED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
#[cfg(all(test, target_os = "linux"))]
pub(crate) fn reset_notice() {
NOTICE_EMITTED.store(false, std::sync::atomic::Ordering::SeqCst);
}
pub(crate) fn reconcile(mut policy: CapabilityPolicy) -> CapabilityPolicy {
if let Some(unavailable) = withhold_unsupported_loopback(&mut policy.process_sandbox) {
if !NOTICE_EMITTED.swap(true, std::sync::atomic::Ordering::SeqCst) {
crate::events::log_warn_meta(
LOOPBACK_UNAVAILABLE_CATEGORY,
&unavailable.notice(),
unavailable.metadata(),
);
}
}
policy
}
#[cfg(test)]
#[path = "loopback_tests.rs"]
mod tests;