use std::collections::HashMap;
use std::sync::Arc;
use sysknife_core::{config::LacsConfig, default_database_path, default_listen_uri};
use sysknife_daemon::audit_forward::{self, AuditForwarder, AuditSinkSpec};
use sysknife_daemon::dispatcher::{resolve_caller, unix_connection_handler};
use sysknife_daemon::policy::PolicyTable;
use sysknife_daemon::state::{DaemonConfig, DaemonState};
use sysknife_daemon::state_collector::RealCommandRunner;
use sysknife_daemon::transport::listen::{bind_unix_listener, ListenTarget, ListenTargetError};
use tokio::net::UnixListener;
use tokio::sync::Semaphore;
const STALE_SWEEP_INTERVAL: std::time::Duration = std::time::Duration::from_secs(300);
const MAX_CONNECTIONS: usize = 16;
#[tokio::main]
async fn main() {
if let Err(e) = run().await {
eprintln!("[sysknife-daemon] FATAL: {e}");
std::process::exit(1);
}
}
async fn run() -> Result<(), Box<dyn std::error::Error>> {
let lacs_config = LacsConfig::load();
lacs_config.apply_defaults_to_env();
let listen_uri = default_listen_uri();
let database_path = default_database_path();
let raw_overrides: HashMap<String, String> = lacs_config
.policy
.as_ref()
.and_then(|p| p.risk_overrides.clone())
.unwrap_or_default();
let policy = PolicyTable::from_overrides(&raw_overrides).inspect_err(|_| {
eprintln!("[sysknife-daemon] policy validation failed");
})?;
if policy.override_count() > 0 {
eprintln!(
"[sysknife-daemon] applying {} risk override(s) from [policy.risk_overrides]:",
policy.override_count()
);
for (action, role) in policy.active_overrides() {
eprintln!("[sysknife-daemon] {action:30} → {role:?}");
}
}
let forwarder: Option<AuditForwarder> = match build_forwarder(lacs_config.audit.as_ref()) {
Ok(f) => f,
Err(e) => {
eprintln!("[sysknife-daemon] audit forwarder config invalid");
return Err(e.into());
}
};
if forwarder.is_some() {
eprintln!("[sysknife-daemon] audit-forward: external sink active");
}
let listen_target = ListenTarget::try_from_uri(&listen_uri)?;
let config = DaemonConfig::new(listen_target.clone(), &database_path);
let state = match build_postgres_audit(lacs_config.storage.as_ref()).await {
Ok(Some(audit)) => {
eprintln!("[sysknife-daemon] storage: using Postgres backend (URL hidden)");
DaemonState::open_with_audit(config, policy, forwarder, audit)
}
Ok(None) => {
eprintln!(
"[sysknife-daemon] storage: using SQLite at {} \
(production deployments should switch to [storage] backend = \"postgres\")",
database_path.display()
);
DaemonState::open_full(config, policy, forwarder)?
}
Err(e) => {
eprintln!("[sysknife-daemon] postgres backend init failed");
return Err(e.into());
}
};
let runner = Arc::new(RealCommandRunner);
let semaphore = Arc::new(Semaphore::new(MAX_CONNECTIONS));
match state.audit.reconcile_interrupted_running().await {
Ok(0) => {}
Ok(n) => eprintln!(
"[sysknife-daemon] reconciled {n} interrupted (Running) transaction(s) as failed after restart"
),
Err(e) => eprintln!(
"[sysknife-daemon] WARNING: could not reconcile interrupted transactions: {e}"
),
}
{
let sweep = state.clone();
tokio::spawn(async move {
let mut ticker = tokio::time::interval(STALE_SWEEP_INTERVAL);
loop {
ticker.tick().await;
match sweep.audit.cleanup_stale_queued().await {
Ok(n) if n > 0 => {
eprintln!(
"[sysknife-daemon] swept {n} stale queued transaction(s) past TTL"
);
}
Ok(_) => {}
Err(e) => eprintln!("[sysknife-daemon] stale-queued sweep failed: {e}"),
}
}
});
}
start_checkpoint_anchoring(&state, &database_path);
match listen_target {
ListenTarget::Unix(path) => {
let std_listener = match bind_unix_listener(&ListenTarget::Unix(path.clone())) {
Ok(listener) => listener,
Err(e) => {
eprintln!("[sysknife-daemon] FATAL: {e}");
if matches!(e, ListenTargetError::Io(ref m) if m.contains("Permission denied"))
{
eprintln!(
"[sysknife-daemon] {} is not writable by uid {}. Either run the \
system service as root (sudo systemctl start sysknife-daemon), or \
point this daemon at a socket you own with \
SYSKNIFE_LISTEN_URI=unix://$XDG_RUNTIME_DIR/sysknife/daemon.sock",
path.parent().unwrap_or(&path).display(),
unsafe { libc::geteuid() },
);
}
std::process::exit(1);
}
};
std_listener.set_nonblocking(true)?;
let listener = UnixListener::from_std(std_listener)?;
eprintln!("[sysknife-daemon] listening on {listen_uri}");
unix_accept_loop(listener, state, runner, semaphore).await;
}
#[cfg(target_os = "linux")]
ListenTarget::Vsock { port } => {
use sysknife_daemon::transport::listen::bind_vsock_listener;
let listener = bind_vsock_listener(port).inspect_err(|_| {
eprintln!("[sysknife-daemon] cannot bind vsock port {port}");
})?;
eprintln!("[sysknife-daemon] listening on {listen_uri}");
vsock_accept_loop(listener, state, runner, semaphore).await;
}
}
Ok(())
}
async fn shutdown_signal() {
use tokio::signal::unix::{signal, SignalKind};
match signal(SignalKind::terminate()) {
Ok(mut sigterm) => {
tokio::select! {
_ = tokio::signal::ctrl_c() => {}
_ = sigterm.recv() => {}
}
}
Err(e) => {
eprintln!(
"[sysknife-daemon] WARNING: cannot install SIGTERM handler ({e}); \
responding to Ctrl-C (SIGINT) only"
);
let _ = tokio::signal::ctrl_c().await;
}
}
}
async fn unix_accept_loop(
listener: UnixListener,
state: sysknife_daemon::state::DaemonState,
runner: Arc<RealCommandRunner>,
semaphore: Arc<Semaphore>,
) {
loop {
tokio::select! {
accept = listener.accept() => {
match accept {
Ok((stream, _addr)) => {
match Arc::clone(&semaphore).try_acquire_owned() {
Ok(permit) => {
let caller = resolve_caller(&stream);
let state = state.clone();
let runner = Arc::clone(&runner);
tokio::spawn(async move {
unix_connection_handler(stream, state, runner, caller).await;
drop(permit);
});
}
Err(_) => {
eprintln!(
"[sysknife-daemon] connection limit ({MAX_CONNECTIONS}) reached; \
dropping new connection"
);
}
}
}
Err(e) => match classify_accept_error(&e) {
AcceptErrorAction::LogTransient => {
eprintln!("[sysknife-daemon] transient accept error: {e}");
}
AcceptErrorAction::BreakFatal => {
eprintln!("[sysknife-daemon] fatal accept error, shutting down: {e}");
break;
}
},
}
}
_ = shutdown_signal() => {
eprintln!("[sysknife-daemon] shutting down");
break;
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AcceptErrorAction {
LogTransient,
BreakFatal,
}
fn classify_accept_error(e: &std::io::Error) -> AcceptErrorAction {
use std::io::ErrorKind;
match e.kind() {
ErrorKind::ConnectionAborted | ErrorKind::ConnectionReset => {
AcceptErrorAction::LogTransient
}
_ => AcceptErrorAction::BreakFatal,
}
}
#[cfg(target_os = "linux")]
async fn vsock_accept_loop(
listener: tokio_vsock::VsockListener,
state: sysknife_daemon::state::DaemonState,
runner: Arc<RealCommandRunner>,
semaphore: Arc<Semaphore>,
) {
use sysknife_daemon::dispatcher::vsock_connection_handler;
loop {
tokio::select! {
accept = listener.accept() => {
match accept {
Ok((stream, addr)) => {
eprintln!("[sysknife-daemon] vsock connection from cid={}", addr.cid());
match Arc::clone(&semaphore).try_acquire_owned() {
Ok(permit) => {
let state = state.clone();
let runner = Arc::clone(&runner);
tokio::spawn(async move {
vsock_connection_handler(stream, state, runner).await;
drop(permit);
});
}
Err(_) => {
eprintln!(
"[sysknife-daemon] connection limit ({MAX_CONNECTIONS}) reached; \
dropping vsock connection"
);
}
}
}
Err(e) => match classify_accept_error(&e) {
AcceptErrorAction::LogTransient => {
eprintln!("[sysknife-daemon] transient vsock accept error: {e}");
}
AcceptErrorAction::BreakFatal => {
eprintln!(
"[sysknife-daemon] fatal vsock accept error, shutting down: {e}"
);
break;
}
},
}
}
_ = shutdown_signal() => {
eprintln!("[sysknife-daemon] shutting down");
break;
}
}
}
}
fn start_checkpoint_anchoring(state: &DaemonState, database_path: &std::path::Path) {
use sysknife_daemon::checkpoint_sink::{
spawn_periodic_anchor, PostgresCheckpointSink, DEFAULT_ANCHOR_INTERVAL,
};
let Ok(db_url) = std::env::var("SYSKNIFE_CHECKPOINT_DB") else {
eprintln!(
"[sysknife-daemon] checkpoint anchoring is OFF (SYSKNIFE_CHECKPOINT_DB unset). \
The audit chain is still signed, but tail truncation and a rewrite by someone \
holding the signing key are NOT detectable without an external anchor."
);
return;
};
let key_path = sysknife_daemon::audit_chain::resolve_audit_key_path(database_path);
let key = match sysknife_daemon::audit_chain::AuditKey::load_or_generate(&key_path) {
Ok(key) => Arc::new(key),
Err(e) => {
eprintln!(
"[sysknife-daemon] checkpoint anchoring DISABLED: loading the audit key \
from {} failed: {e}",
key_path.display()
);
return;
}
};
let audit = Arc::clone(&state.audit);
tokio::spawn(async move {
match PostgresCheckpointSink::connect(&db_url).await {
Ok(sink) => {
eprintln!(
"[sysknife-daemon] checkpoint anchoring ON (every {}s)",
DEFAULT_ANCHOR_INTERVAL.as_secs()
);
spawn_periodic_anchor(audit, key, Arc::new(sink), DEFAULT_ANCHOR_INTERVAL);
}
Err(e) => {
eprintln!(
"[sysknife-daemon] checkpoint anchoring DISABLED: connecting to the \
checkpoint database failed: {e}"
);
}
}
});
}
fn build_forwarder(
audit: Option<&sysknife_core::config::AuditSection>,
) -> Result<Option<AuditForwarder>, std::io::Error> {
let Some(audit) = audit else {
return Ok(None);
};
let Some(forward) = audit.forward.as_ref() else {
return Ok(None);
};
let Some(syslog) = forward.syslog.as_ref() else {
return Ok(None);
};
let host: std::net::SocketAddr = syslog.host.parse().map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"[audit.forward.syslog] host {:?} is not a valid host:port: {e}",
syslog.host
),
)
})?;
Ok(Some(audit_forward::spawn(AuditSinkSpec::SyslogUdp {
host,
facility: syslog.facility,
})))
}
async fn build_postgres_audit(
storage: Option<&sysknife_core::config::StorageSection>,
) -> Result<Option<std::sync::Arc<dyn sysknife_daemon::store::AuditStore>>, std::io::Error> {
use sysknife_daemon::audit_chain;
use sysknife_daemon::store::postgres::PostgresConfig;
use sysknife_daemon::store::PostgresStore;
use sysknife_core::config::StorageBackend;
let Some(storage) = storage else {
return Ok(None);
};
let parsed = storage
.parsed()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
let (url, pool) = match parsed {
StorageBackend::Sqlite => return Ok(None),
StorageBackend::Postgres { url, pool } => (url, pool),
};
let mut cfg = PostgresConfig {
url,
..PostgresConfig::default()
};
if let Some(n) = pool.max_connections {
cfg.max_connections = n;
}
if let Some(s) = pool.acquire_timeout_secs {
cfg.acquire_timeout = std::time::Duration::from_secs(s);
}
if let Some(c) = pool.statement_cache_capacity {
cfg.statement_cache_capacity = c;
}
let key_path = std::env::var("SYSKNIFE_AUDIT_KEY_PATH")
.map(std::path::PathBuf::from)
.unwrap_or_else(|_| {
sysknife_core::default_database_path()
.parent()
.unwrap_or_else(|| std::path::Path::new("."))
.join("audit-key")
});
let key = audit_chain::AuditKey::load_or_generate(&key_path)
.map_err(|e| std::io::Error::other(format!("audit key load failed: {e}")))?;
let store = PostgresStore::connect(&cfg, std::sync::Arc::new(key))
.await
.map_err(|e| std::io::Error::other(format!("postgres connect failed: {e}")))?;
Ok(Some(std::sync::Arc::new(store)))
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use tokio::sync::Semaphore;
#[test]
#[allow(clippy::assertions_on_constants)]
fn max_connections_is_reasonable() {
assert!(
super::MAX_CONNECTIONS >= 4,
"MAX_CONNECTIONS {} too low; need at least one connection per shell + headroom",
super::MAX_CONNECTIONS
);
assert!(
super::MAX_CONNECTIONS <= 64,
"MAX_CONNECTIONS {} too high; each connection holds DB state",
super::MAX_CONNECTIONS
);
}
#[test]
fn classify_accept_error_treats_peer_resets_as_transient() {
use std::io::{Error, ErrorKind};
for kind in [ErrorKind::ConnectionAborted, ErrorKind::ConnectionReset] {
let err = Error::new(kind, "peer hung up");
assert_eq!(
super::classify_accept_error(&err),
super::AcceptErrorAction::LogTransient,
"{kind:?} must keep the loop alive"
);
}
}
#[test]
fn classify_accept_error_treats_other_kinds_as_fatal() {
use std::io::{Error, ErrorKind};
for kind in [
ErrorKind::PermissionDenied,
ErrorKind::OutOfMemory,
ErrorKind::AddrNotAvailable,
ErrorKind::Other,
ErrorKind::Interrupted,
ErrorKind::WouldBlock,
] {
let err = Error::new(kind, "fatal");
assert_eq!(
super::classify_accept_error(&err),
super::AcceptErrorAction::BreakFatal,
"{kind:?} must break the accept loop so the supervisor restarts the daemon"
);
}
}
#[test]
fn semaphore_backpressure_drops_excess_acquires_and_recovers_on_release() {
const SLOTS: usize = 2;
let sem = Arc::new(Semaphore::new(SLOTS));
let p1 = Arc::clone(&sem)
.try_acquire_owned()
.expect("first slot must acquire");
let p2 = Arc::clone(&sem)
.try_acquire_owned()
.expect("second slot must acquire");
assert!(
Arc::clone(&sem).try_acquire_owned().is_err(),
"{}+1 acquires must be refused while all permits are held",
SLOTS
);
drop(p1);
let p3 = Arc::clone(&sem)
.try_acquire_owned()
.expect("released permit must allow one more acquire");
assert!(
Arc::clone(&sem).try_acquire_owned().is_err(),
"with the recovered slot also held, further acquires must be refused"
);
drop(p2);
drop(p3);
let mut reclaimed = Vec::with_capacity(SLOTS);
for _ in 0..SLOTS {
reclaimed.push(
Arc::clone(&sem)
.try_acquire_owned()
.expect("after releasing both, all permits are reclaimable"),
);
}
assert!(
Arc::clone(&sem).try_acquire_owned().is_err(),
"with all reclaimed permits held, further acquires must be refused"
);
drop(reclaimed);
}
}