use std::path::PathBuf;
use std::sync::Arc;
use clap::Parser;
use tokio::sync::Mutex;
use zlayer_overlayd::server::OverlaydServer;
use zlayer_overlayd::transport;
use zlayer_paths::ZLayerDirs;
use zlayer_types::overlayd::{OverlaydFrame, OverlaydRequest};
#[cfg(windows)]
mod service;
#[derive(Debug, Parser)]
#[command(name = "zlayer-overlayd", about, version)]
struct Args {
#[arg(long, value_name = "PATH")]
data_dir: Option<PathBuf>,
#[arg(long, value_name = "PATH")]
socket: Option<PathBuf>,
#[arg(long, hide = true)]
#[cfg_attr(not(windows), allow(dead_code))]
service: bool,
#[arg(long, hide = true)]
build_id: bool,
}
fn main() -> anyhow::Result<()> {
let args = Args::parse();
if args.build_id {
println!("{}", env!("ZLAYER_OVERLAYD_BUILD_ID"));
return Ok(());
}
let _log_guards = match zlayer_observability::init_common_logging(
"overlayd",
zlayer_observability::CommonLoggingOptions::default(),
) {
Ok(guards) => Some(guards),
Err(e) => {
eprintln!("overlayd: shared logging init failed ({e}); falling back to stderr");
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.init();
None
}
};
let data_dir = args.data_dir.unwrap_or_else(ZLayerDirs::default_data_dir);
let socket = args
.socket
.unwrap_or_else(|| PathBuf::from(ZLayerDirs::default_overlayd_socket_path_for(&data_dir)));
tracing::info!(
data_dir = %data_dir.display(),
socket = %socket.display(),
"starting zlayer-overlayd"
);
#[cfg(windows)]
if args.service {
return service::run_as_overlayd_service(data_dir, socket);
}
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.thread_name("zlayer-overlayd-worker")
.build()?;
runtime.block_on(async move {
let (external_tx, external_rx) = tokio::sync::oneshot::channel::<()>();
tokio::spawn(async move {
#[cfg(unix)]
{
use tokio::signal::unix::{signal, SignalKind};
match signal(SignalKind::terminate()) {
Ok(mut sigterm) => {
tokio::select! {
r = tokio::signal::ctrl_c() => {
if r.is_ok() {
tracing::info!(
"overlayd: Ctrl-C (SIGINT) received; signaling shutdown"
);
}
}
_ = sigterm.recv() => {
tracing::info!("overlayd: SIGTERM received; signaling shutdown");
}
}
}
Err(e) => {
tracing::warn!(error = %e, "overlayd: failed to install SIGTERM handler");
if tokio::signal::ctrl_c().await.is_ok() {
tracing::info!("overlayd: Ctrl-C received; signaling shutdown");
}
}
}
}
#[cfg(not(unix))]
if tokio::signal::ctrl_c().await.is_ok() {
tracing::info!("overlayd: Ctrl-C received; signaling shutdown");
}
let _ = external_tx.send(());
});
run_overlayd(data_dir, socket, external_rx).await
})
}
async fn run_overlayd(
data_dir: PathBuf,
socket: PathBuf,
external_shutdown: tokio::sync::oneshot::Receiver<()>,
) -> anyhow::Result<()> {
let dirs = ZLayerDirs::new(data_dir.clone());
let uapi_dir = dirs.wireguard();
#[cfg(not(windows))]
normalize_uapi_sock_dir(&uapi_dir);
let server = OverlaydServer::new(data_dir).with_uapi_sock_dir(uapi_dir);
let server = Arc::new(Mutex::new(server));
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
let shutdown_tx = Arc::new(Mutex::new(Some(shutdown_tx)));
let serve = {
let server = Arc::clone(&server);
let shutdown_tx = Arc::clone(&shutdown_tx);
transport::serve(&socket, move |mut conn| {
let server = Arc::clone(&server);
let shutdown_tx = Arc::clone(&shutdown_tx);
async move {
loop {
let frame = match conn.recv().await {
Ok(f) => f,
Err(zlayer_overlayd::OverlaydError::Closed) => break,
Err(e) => {
tracing::warn!(error = %e, "overlayd: recv failed; closing connection");
break;
}
};
let OverlaydFrame::Request { id, request } = frame else {
tracing::warn!("overlayd: received non-Request frame; ignoring");
continue;
};
let (response, shutdown) = {
let mut guard = server.lock().await;
let response = guard.handle(request).await;
(response, guard.shutdown_requested())
};
if let Err(e) = conn.send(&OverlaydFrame::Response { id, response }).await {
tracing::warn!(error = %e, "overlayd: send failed; closing connection");
break;
}
if shutdown {
tracing::info!("overlayd: shutdown requested; signaling exit");
if let Some(tx) = shutdown_tx.lock().await.take() {
let _ = tx.send(());
}
break;
}
}
}
})
};
tokio::select! {
res = serve => {
res?;
}
_ = shutdown_rx => {
tracing::info!("overlayd: exiting after graceful shutdown (in-band request)");
}
_ = external_shutdown => {
let mut guard = server.lock().await;
if guard.shutdown_requested() {
tracing::info!(
"overlayd: exiting after graceful shutdown \
(external signal; teardown already performed)"
);
} else {
tracing::info!(
"overlayd: external signal received; running global overlay teardown"
);
let _ = guard.handle(OverlaydRequest::Shutdown).await;
tracing::info!("overlayd: exiting after graceful shutdown (external signal)");
}
}
}
Ok(())
}
#[cfg(not(windows))]
fn sweep_stale_uapi_sockets(dir: &std::path::Path) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
if name.starts_with("zl-") && name.ends_with(".sock") {
let _ = std::fs::remove_file(entry.path());
}
}
}
#[cfg(not(windows))]
fn normalize_uapi_sock_dir(dir: &std::path::Path) {
use std::os::unix::fs::MetadataExt;
use std::os::unix::fs::PermissionsExt;
static ASIDE_SEQ: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
if std::env::var_os("ZLAYER_ROOTLESS").is_some() {
if let Err(e) = std::fs::create_dir_all(dir) {
tracing::warn!(dir = %dir.display(), error = %e, "failed to create UAPI socket dir (rootless)");
return;
}
let _ = std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700));
sweep_stale_uapi_sockets(dir);
return;
}
if let Ok(meta) = std::fs::metadata(dir) {
if meta.uid() != 0 {
if let Some(parent) = dir.parent() {
let name = dir.file_name().map_or_else(
|| "wireguard".to_string(),
|n| n.to_string_lossy().into_owned(),
);
let seq = ASIDE_SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let aside = parent.join(format!(".{name}.stale-{}-{seq}", std::process::id()));
match std::fs::rename(dir, &aside) {
Ok(()) => tracing::warn!(
dir = %dir.display(),
uid = meta.uid(),
moved_to = %aside.display(),
"UAPI socket dir was not root-owned; moved aside and recreating root-owned"
),
Err(e) => tracing::warn!(
dir = %dir.display(),
uid = meta.uid(),
error = %e,
"UAPI socket dir is not root-owned and could not be moved aside; \
overlay setup may fail to clear a stale socket"
),
}
}
}
}
if let Err(e) = std::fs::create_dir_all(dir) {
tracing::warn!(dir = %dir.display(), error = %e, "failed to create UAPI socket dir");
return;
}
let _ = std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700));
sweep_stale_uapi_sockets(dir);
}
#[cfg(all(test, not(windows)))]
mod tests {
use super::*;
use std::fs;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::MetadataExt;
fn unique_tmp(tag: &str) -> std::path::PathBuf {
static SEQ: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
let seq = SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
std::env::temp_dir().join(format!(
"zlayer-overlayd-uapitest-{tag}-{}-{seq}",
std::process::id()
))
}
#[test]
fn sweep_removes_only_zl_sockets() {
let dir = unique_tmp("sweep");
fs::create_dir_all(&dir).unwrap();
let stale_global = dir.join("zl-124aeda7be-g.sock");
let stale_service = dir.join("zl-81c6bc17c7-s.sock");
let keep = dir.join("keep.txt");
let keep_sock = dir.join("other.sock"); for p in [&stale_global, &stale_service, &keep, &keep_sock] {
fs::write(p, b"x").unwrap();
}
sweep_stale_uapi_sockets(&dir);
assert!(
!stale_global.exists(),
"stale global socket should be swept"
);
assert!(
!stale_service.exists(),
"stale service socket should be swept"
);
assert!(keep.exists(), "non-socket file must be preserved");
assert!(
keep_sock.exists(),
"a .sock without the zl- prefix must be preserved"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn normalize_leaves_usable_root_owned_dir_without_stale_sockets() {
let dir = unique_tmp("normalize");
fs::create_dir_all(&dir).unwrap();
let stale = dir.join("zl-124aeda7be-g.sock");
fs::write(&stale, b"x").unwrap();
normalize_uapi_sock_dir(&dir);
assert!(dir.exists(), "uapi dir must exist after normalize");
assert!(
!dir.join("zl-124aeda7be-g.sock").exists(),
"stale socket must not survive in the live dir after normalize"
);
let _ = fs::remove_dir_all(&dir);
reap_aside_siblings(&dir);
}
fn reap_aside_siblings(dir: &std::path::Path) {
if let Some(parent) = dir.parent() {
if let Some(name) = dir.file_name().map(|n| n.to_string_lossy().into_owned()) {
let prefix = format!(".{name}.stale-");
if let Ok(entries) = fs::read_dir(parent) {
for entry in entries.flatten() {
if entry.file_name().to_string_lossy().starts_with(&prefix) {
let _ = fs::remove_dir_all(entry.path());
}
}
}
}
}
}
#[test]
#[allow(unsafe_code)] fn normalize_reclaims_foreign_owned_dir_root_only() {
let is_root = unsafe { libc::geteuid() } == 0;
if !is_root {
eprintln!("skipping normalize_reclaims_foreign_owned_dir_root_only: requires root");
return;
}
let dir = unique_tmp("foreign");
fs::create_dir_all(&dir).unwrap();
let stale = dir.join("zl-124aeda7be-g.sock");
fs::write(&stale, b"x").unwrap();
let c_dir = std::ffi::CString::new(dir.as_os_str().as_bytes()).unwrap();
let rc = unsafe { libc::chown(c_dir.as_ptr(), 1, 1) };
assert_eq!(rc, 0, "test setup: chown to uid 1 must succeed as root");
assert_ne!(
fs::metadata(&dir).unwrap().uid(),
0,
"test setup: dir should now be foreign-owned"
);
normalize_uapi_sock_dir(&dir);
let meta = fs::metadata(&dir).expect("uapi dir must exist after normalize");
assert_eq!(
meta.uid(),
0,
"normalize must hand back a root-owned uapi dir"
);
assert!(
!dir.join("zl-124aeda7be-g.sock").exists(),
"the stale socket must be gone from the reclaimed dir"
);
let _ = fs::remove_dir_all(&dir);
reap_aside_siblings(&dir);
}
}