//! Swarm-level controller daemon. Runs as the unprivileged //! `swarm-controller` user on whichever host the operator flips //! `services.hyperhive.swarm.controller.enable` on, and serves HTTP over a //! unix socket that the hive-gateway's nginx proxies to. //! //! **Today it serves one endpoint and owns no state.** That is deliberate: //! this slice exists to make the *unit* real — service user, runtime and //! state directories, socket, nginx reachability — so the swarm-level //! surfaces that follow have somewhere to land. Guessing those surfaces //! now would bake in a shape nobody has agreed to. //! //! Distinct from `hive-c0re`, which is per-hive: c0re owns the agents on //! one host, this owns what is true across hives. use std::os::unix::fs::PermissionsExt as _; use std::path::PathBuf; use anyhow::{Context, Result}; use axum::{Router, routing::get}; /// Where the daemon binds, overridable via `SWARM_CONTROLLER_SOCKET`. /// /// A compiled-in default is legitimate here and is *not* the mistake that /// a hardcoded remote address would be: this is a path this process /// **creates**, not an address it hopes to find something at. systemd's /// `RuntimeDirectory=swarm-controller` makes the parent exist before /// `ExecStart`, so the default names a directory the unit just produced. /// /// The directory is its own — deliberately not shared with hive-c0re's /// `/run/hyperhive`. The socket is `0666`, so its directory is the only /// access control it has; co-locating it with c0re's admin socket would /// put both within reach of whatever can reach either. nginx runs on the /// host, so nothing narrows its reach for you. const DEFAULT_SOCKET: &str = "/run/swarm-controller/controller.sock"; fn socket_path() -> PathBuf { std::env::var_os("SWARM_CONTROLLER_SOCKET") .map_or_else(|| PathBuf::from(DEFAULT_SOCKET), PathBuf::from) } /// Liveness probe. Returns the build's version so an operator can tell /// *which* controller answered without shelling onto the host. async fn health() -> &'static str { concat!("swarm-controller ", env!("CARGO_PKG_VERSION"), "\n") } #[tokio::main] async fn main() -> Result<()> { tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), ) .init(); let path = socket_path(); // `RuntimeDirectoryPreserve=yes` keeps the directory across a restart, // so a socket file from the previous run can outlive the process that // owned it and `bind` would fail with EADDRINUSE. Unlinking a stale // socket is safe precisely because the directory is ours alone: nothing // else can have put a file at this path. if let Err(e) = std::fs::remove_file(&path) && e.kind() != std::io::ErrorKind::NotFound { return Err(e).with_context(|| format!("clearing stale socket at {}", path.display())); } let listener = tokio::net::UnixListener::bind(&path) .with_context(|| format!("binding {}", path.display()))?; // `bind` leaves the socket 0755, and connecting needs write — the // gateway's nginx is a different user, so it would be locked out. // 0666 matches how hive-c0re publishes the per-agent sockets // (`socket_server::start`), and rests on the same argument: **the // containing directory is the access control, not the socket mode.** // This directory holds one socket and is bind-mounted into exactly // one container. That is also why it must not be shared with // hive-c0re's `/run/hyperhive` — with a 0666 socket, a directory // that carries more than it should is the whole vulnerability. std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o666)) .with_context(|| format!("chmod {}", path.display()))?; tracing::info!(socket = %path.display(), "swarm-controller listening"); let app = Router::new().route("/health", get(health)); axum::serve(listener, app) .await .context("serving swarm-controller") } #[cfg(test)] mod tests { use super::DEFAULT_SOCKET; use std::path::Path; /// The socket must not share a directory with anything else, because /// the socket is `0666` and the directory is therefore the only access /// control it has. `/run/hyperhive` in particular holds hive-c0re's /// **admin** socket, and nginx — a host service — is bounded by /// nothing but the directory itself. /// /// A test rather than a comment: the failure this guards against is a /// one-word edit that looks tidier and reads fine in review. #[test] fn socket_lives_in_its_own_runtime_dir() { let parent = Path::new(DEFAULT_SOCKET) .parent() .expect("socket path has a parent directory"); assert_eq!( parent, Path::new("/run/swarm-controller"), "the socket's directory is its access control — moving it under a shared \ directory (notably /run/hyperhive, which holds the host admin socket) \ exposes everything else in that directory to the gateway's nginx" ); } }