harness: opt-in UnixListener bind via HIVE_WEB_SOCKET (#784 phase 1)
phase 1 of #784 (unix-domain agent web UI for #14 prerequisite). backwards-compatible: when HIVE_WEB_SOCKET is set + non-empty, bind a UnixListener at that path instead of the legacy TCP bind on HIVE_PORT. empty env var treated as unset so a stray HIVE_WEB_SOCKET= doesn't trap an un-bindable empty path. bind_unix helper: - mkdir -p the socket parent (covers first-boot fresh /run/hive-agent/ bind-mount target) - best-effort unlink of stale socket (clean exit removes it, crash leaves it; bind(2) refuses to overwrite) - mode 0o660 so gateway peers in the same unix group can connect (the bind-mount source dir ACL is the real gate; perms are defence in depth) axum 0.8's Listener trait covers tokio::net::UnixListener directly (no extra feature/dep required). next phases (separate PRs): - step 2: c0re bind-mounts /run/hive-agent/ + writes agent-sockets.json alongside agent-ports.json - step 3: gateway proxy_pass http://unix:… (atlas) - step 4: drop TCP bind once gateway no longer needs it
This commit is contained in:
parent
224385af37
commit
0ef79b8032
1 changed files with 56 additions and 3 deletions
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
use std::convert::Infallible;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
|
@ -78,7 +78,19 @@ pub type Flavor = mcp::Flavor;
|
|||
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the TCP listener cannot bind to the given port.
|
||||
/// Returns an error if neither the TCP listener (default) nor the
|
||||
/// unix-socket bind (`HIVE_WEB_SOCKET`, if set) can be acquired, or
|
||||
/// if `HIVE_STATIC_DIR` is missing.
|
||||
///
|
||||
/// # Binding modes (#784 phase 1)
|
||||
///
|
||||
/// When `HIVE_WEB_SOCKET` is set + non-empty, bind a `UnixListener`
|
||||
/// at that path so the gateway can `proxy_pass unix:…` instead of
|
||||
/// reaching us over a TCP loopback that won't work post-#14 (private
|
||||
/// netns). When the env var is unset, fall back to TCP bind on `port`
|
||||
/// — the legacy path the gateway's `agent-ports.json` map drives. The
|
||||
/// gateway can transition to socket upstreams independently of any
|
||||
/// agent re-binding because the env var is opt-in per agent.
|
||||
pub async fn serve(
|
||||
label: String,
|
||||
port: u16,
|
||||
|
|
@ -138,13 +150,54 @@ pub async fn serve(
|
|||
// hyperhive.frontend.mergedDist in nix).
|
||||
.fallback_service(ServeDir::new(&static_dir))
|
||||
.with_state(state);
|
||||
// `HIVE_WEB_SOCKET` opt-in (#784 phase 1): when set, bind a
|
||||
// `UnixListener` at the given path. Empty string treated as
|
||||
// unset so a stray `HIVE_WEB_SOCKET=` doesn't trap us into an
|
||||
// un-bindable empty path. Falls through to the TCP path below
|
||||
// otherwise.
|
||||
if let Some(socket_path) = std::env::var_os("HIVE_WEB_SOCKET")
|
||||
&& !socket_path.is_empty()
|
||||
{
|
||||
let path = PathBuf::from(socket_path);
|
||||
let listener = bind_unix(&path)?;
|
||||
tracing::info!(socket = %path.display(), "web UI listening on unix socket");
|
||||
axum::serve(listener, app).await?;
|
||||
return Ok(());
|
||||
}
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||
let listener = bind_with_retry(addr, "web UI").await?;
|
||||
tracing::info!(%port, "web UI listening");
|
||||
tracing::info!(%port, "web UI listening on tcp");
|
||||
axum::serve(listener, app).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Bind a `UnixListener` at `path`. Best-effort unlinks any stale
|
||||
/// socket left over from a previous (crashed) harness — clean exit
|
||||
/// removes it, but `bind(2)` refuses to overwrite an existing file.
|
||||
/// Also `mkdir -p` the parent so a freshly-created `/run/hive-agent/`
|
||||
/// bind-mount target works on first boot.
|
||||
///
|
||||
/// Permissions: mode `0o660` so peers in the same unix group (the
|
||||
/// gateway container, when bind-mounting the socket dir with a
|
||||
/// shared group) can `connect(2)`. The bind-mount source dir's
|
||||
/// ownership + ACL is the real access gate; this is defence-in-depth.
|
||||
fn bind_unix(path: &Path) -> Result<tokio::net::UnixListener> {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.with_context(|| format!("create socket parent dir {}", parent.display()))?;
|
||||
}
|
||||
// Best-effort: ENOENT is fine (no stale file); any other error
|
||||
// surfaces via the bind below with a clearer "AddrInUse" / perms
|
||||
// message than a partial cleanup would.
|
||||
let _ = std::fs::remove_file(path);
|
||||
let listener = tokio::net::UnixListener::bind(path)
|
||||
.with_context(|| format!("bind unix socket at {}", path.display()))?;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o660))
|
||||
.with_context(|| format!("set perms on {}", path.display()))?;
|
||||
Ok(listener)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Static assets + state snapshot
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Reference in a new issue