use std::os::fd::{FromRawFd, OwnedFd};
use std::path::PathBuf;
use std::process::Command;
use std::sync::Arc;
use std::time::Duration;
use anyhow::{Context, Result};
use arcbox_constants::ports::SSH_AUTH_RELAY_PORT;
use arcbox_core::{DEFAULT_MACHINE_NAME, Runtime, VmLifecycleState};
use arcbox_transport::vsock::{VsockShutdown, VsockStream};
use tokio::io::{AsyncReadExt, copy_bidirectional};
use tokio::net::UnixStream;
use tokio::sync::watch;
use tokio_util::sync::CancellationToken;
use tracing::{debug, info, warn};
use crate::context::DaemonContext;
const POOL_SIZE: usize = 8;
const REDIAL_BACKOFF: Duration = Duration::from_secs(1);
pub fn spawn(ctx: &DaemonContext, runtime: &Arc<Runtime>) {
if !forwarding_enabled() {
info!("ssh-auth agent forwarding disabled via ARCBOX_SSH_AGENT_FORWARDING");
return;
}
for id in 0..POOL_SIZE {
let runtime = Arc::clone(runtime);
let shutdown = ctx.shutdown.clone();
tokio::spawn(run_worker(id, runtime, shutdown));
}
info!(pool = POOL_SIZE, "ssh-auth agent forwarding enabled");
}
fn forwarding_enabled() -> bool {
match std::env::var("ARCBOX_SSH_AGENT_FORWARDING") {
Ok(v) => !matches!(
v.trim().to_ascii_lowercase().as_str(),
"0" | "false" | "off" | "no"
),
Err(_) => true,
}
}
async fn run_worker(id: usize, runtime: Arc<Runtime>, shutdown: CancellationToken) {
let mut state = runtime.subscribe_system_vm_state();
loop {
if !wait_until_ready(&mut state, &shutdown).await {
return;
}
match serve_once(&runtime, &shutdown).await {
Ok(()) => {}
Err(e) => {
debug!(worker = id, error = %e, "ssh-auth: park attempt failed; retrying");
if !sleep_unless_cancelled(REDIAL_BACKOFF, &shutdown).await {
return;
}
}
}
}
}
async fn serve_once(runtime: &Arc<Runtime>, shutdown: &CancellationToken) -> Result<()> {
let mut slot = dial_slot(runtime).await?;
let mut marker = [0u8; 1];
let read = tokio::select! {
biased;
() = shutdown.cancelled() => return Ok(()),
r = slot.read_exact(&mut marker) => r,
};
read.context("parked slot closed before a container paired")?;
let Some(sock_path) = resolve_ssh_auth_sock() else {
warn!(
"ssh-auth: no ssh-agent found on the Mac (SSH_AUTH_SOCK unset); \
closing the forward so the container reports a clear error"
);
return Ok(());
};
let mut agent = match UnixStream::connect(&sock_path).await {
Ok(s) => s,
Err(e) => {
warn!(path = %sock_path.display(), error = %e, "ssh-auth: cannot reach the Mac ssh-agent");
return Ok(());
}
};
tokio::select! {
biased;
() = shutdown.cancelled() => {}
r = copy_bidirectional(&mut slot, &mut agent) => {
if let Err(e) = r {
debug!(error = %e, "ssh-auth: forward copy ended with error");
}
}
}
Ok(())
}
async fn dial_slot(runtime: &Arc<Runtime>) -> Result<VsockStream> {
let rt = Arc::clone(runtime);
let fd = tokio::task::spawn_blocking(move || {
rt.connect_vsock_port(DEFAULT_MACHINE_NAME, SSH_AUTH_RELAY_PORT)
})
.await??;
let owned = unsafe { OwnedFd::from_raw_fd(fd) };
let stream = VsockStream::from_fd_with_shutdown(owned, VsockShutdown::CloseOnDropOnly)?;
Ok(stream)
}
fn resolve_ssh_auth_sock() -> Option<PathBuf> {
for var in ["ARCBOX_SSH_AUTH_SOCK", "SSH_AUTH_SOCK"] {
if let Some(value) = std::env::var_os(var) {
if !value.is_empty() {
return Some(PathBuf::from(value));
}
}
}
launchctl_getenv("SSH_AUTH_SOCK")
}
fn launchctl_getenv(name: &str) -> Option<PathBuf> {
let output = Command::new("/bin/launchctl")
.arg("getenv")
.arg(name)
.output()
.ok()?;
if !output.status.success() {
return None;
}
let value = String::from_utf8_lossy(&output.stdout).trim().to_string();
if value.is_empty() || value == "(null)" {
None
} else {
Some(PathBuf::from(value))
}
}
async fn wait_until_ready(
state: &mut watch::Receiver<VmLifecycleState>,
shutdown: &CancellationToken,
) -> bool {
loop {
if shutdown.is_cancelled() {
return false;
}
if state.borrow_and_update().is_ready() {
return true;
}
tokio::select! {
biased;
() = shutdown.cancelled() => return false,
changed = state.changed() => {
if changed.is_err() {
return false;
}
}
}
}
}
async fn sleep_unless_cancelled(duration: Duration, shutdown: &CancellationToken) -> bool {
tokio::select! {
biased;
() = shutdown.cancelled() => false,
() = tokio::time::sleep(duration) => true,
}
}
#[cfg(test)]
mod tests {
use super::forwarding_enabled;
#[test]
fn env_gate_defaults_on_and_honors_falsy_values() {
let restore = std::env::var_os("ARCBOX_SSH_AGENT_FORWARDING");
unsafe { std::env::remove_var("ARCBOX_SSH_AGENT_FORWARDING") };
assert!(forwarding_enabled(), "unset means enabled");
for falsy in ["0", "false", "off", "no", "OFF", " false "] {
unsafe { std::env::set_var("ARCBOX_SSH_AGENT_FORWARDING", falsy) };
assert!(!forwarding_enabled(), "{falsy:?} should disable");
}
for truthy in ["1", "true", "on", "yes", "anything"] {
unsafe { std::env::set_var("ARCBOX_SSH_AGENT_FORWARDING", truthy) };
assert!(forwarding_enabled(), "{truthy:?} should enable");
}
match restore {
Some(v) => unsafe { std::env::set_var("ARCBOX_SSH_AGENT_FORWARDING", v) },
None => unsafe { std::env::remove_var("ARCBOX_SSH_AGENT_FORWARDING") },
}
}
}