use std::net::SocketAddr;
use std::path::PathBuf;
use std::time::Duration;
use crate::clock::ClockSource;
use crate::signal_install::SignalHandlerMode;
use crate::tracker::EvictionPolicy;
pub const DEFAULT_RECOVERY_DEBOUNCE_MS: u64 = 1000;
pub const DEFAULT_SOCKET_MODE: u32 = 0o600;
pub const DEFAULT_READ_TIMEOUT_MS: u64 = 100;
pub const MIN_THRESHOLD_MS: u64 = 10;
pub const DEFAULT_PROM_RATE_LIMIT_PER_SEC: u32 = 5;
pub const DEFAULT_PROM_RATE_LIMIT_BURST: u32 = 10;
#[cfg(not(feature = "compile-time-config"))]
pub const DEFAULT_MAX_BEAT_RATE: u32 = 100;
#[cfg(not(feature = "compile-time-config"))]
pub const DEFAULT_GLOBAL_BEAT_RATE: u32 = 5_000;
#[cfg(not(feature = "compile-time-config"))]
pub const DEFAULT_GLOBAL_BEAT_BURST: u32 = 10_000;
#[cfg(not(feature = "compile-time-config"))]
pub const DEFAULT_UDS_RCVBUF_BYTES: u32 = 1_048_576;
pub const DEFAULT_SHUTDOWN_GRACE_MS: u64 = 5_000;
pub const MIN_SHUTDOWN_GRACE_MS: u64 = 100;
pub const DEFAULT_RECOVERY_CAPTURE_BYTES: u32 = 4096;
pub const MAX_RECOVERY_CAPTURE_BYTES: u32 = 1024 * 1024;
pub const MIN_ITERATION_BUDGET_MS: u64 = 50;
pub const MAX_ITERATION_BUDGET_MS: u64 = 60_000;
pub const MIN_SCRAPE_BUDGET_MS: u64 = 50;
pub const MAX_SCRAPE_BUDGET_MS: u64 = 60_000;
#[cfg(not(feature = "compile-time-config"))]
pub const DEFAULT_AUDIT_FSYNC_BUDGET_MS: u32 = 50;
#[cfg(not(feature = "compile-time-config"))]
pub const DEFAULT_AUDIT_SYNC_INTERVAL_MS: u32 = 0;
#[cfg(not(feature = "compile-time-config"))]
pub const DEFAULT_AUDIT_ROTATION_BUDGET_MS: u32 = 50;
#[derive(Clone, Debug)]
pub struct Config {
pub socket: PathBuf,
pub threshold: Duration,
pub recovery_exec_cmd: Option<String>,
pub recovery_exec_file: Option<PathBuf>,
pub recovery_debounce: Duration,
pub recovery_env: Vec<String>,
pub recovery_inherit_env: bool,
pub file_export: Option<PathBuf>,
pub export_file_max_bytes: Option<u64>,
pub export_file_sync_every: u32,
pub prom_addr: Option<SocketAddr>,
pub prom_token_file: Option<PathBuf>,
pub shutdown_after: Option<Duration>,
pub shutdown_grace: Duration,
pub recovery_timeout: Option<Duration>,
pub socket_mode: u32,
pub read_timeout: Duration,
pub tracker_capacity: usize,
pub tracker_eviction_policy: EvictionPolicy,
pub eviction_scan_window: usize,
pub udp_port: Option<u16>,
pub udp_bind_addr: Option<std::net::IpAddr>,
pub secure_key_file: Option<PathBuf>,
pub accepted_key_file: Option<PathBuf>,
pub master_key_file: Option<PathBuf>,
pub max_beat_rate: Option<u32>,
pub global_beat_rate: u32,
pub global_beat_burst: u32,
pub uds_rcvbuf_bytes: u32,
pub heartbeat_file: Option<PathBuf>,
pub self_watchdog: Option<Duration>,
pub hw_watchdog: Option<PathBuf>,
pub prom_rate_limit_per_sec: u32,
pub prom_rate_limit_burst: u32,
pub i_accept_plaintext_udp: bool,
pub i_accept_recovery_on_secure_udp: bool,
pub i_accept_recovery_on_plaintext_udp: bool,
pub i_accept_secure_udp_non_loopback: bool,
pub allow_cross_namespace_agents: bool,
pub strict_namespace_check: bool,
pub recovery_audit_file: Option<PathBuf>,
pub recovery_audit_max_bytes: Option<u64>,
pub recovery_audit_sync_every: u32,
pub recovery_capture_stdio: bool,
pub recovery_capture_bytes: u32,
pub iteration_budget: Duration,
pub scrape_budget: Duration,
pub audit_fsync_budget_ms: u32,
pub audit_sync_interval_ms: u32,
pub audit_rotation_budget_ms: u32,
#[cfg(feature = "test-hooks")]
pub inject_wedge_ms: Option<u64>,
pub clock_source: ClockSource,
pub signal_handler_mode: SignalHandlerMode,
}
#[derive(Debug)]
pub enum ConfigError {
MissingValue(&'static str),
MissingRequired(&'static str),
UnknownFlag(String),
BadInteger {
flag: &'static str,
raw: String,
},
BadSocketMode(String),
BadAddr(String),
BadValue {
flag: &'static str,
raw: String,
},
HelpRequested,
ThresholdTooLow {
value: u64,
min: u64,
},
MutuallyExclusive {
a: &'static str,
b: &'static str,
},
RemovedFlag {
flag: &'static str,
replacement: &'static str,
},
PromAddrRequiresToken,
RecoveryCaptureBytesTooLarge {
value: u32,
max: u32,
},
RecoveryCaptureRequiresRecovery,
ShutdownGraceTooLow {
value: u64,
min: u64,
},
ShellRecoveryNotCompiledIn,
RecoveryRequiresAuthenticatedTransport {
udp_addr: String,
},
SecureUdpRequiresLoopbackBind {
udp_addr: String,
},
IterationBudgetOutOfRange {
value: u64,
min: u64,
max: u64,
},
ScrapeBudgetOutOfRange {
value: u64,
min: u64,
max: u64,
},
EvictionScanWindowOutOfRange {
value: usize,
min: usize,
max: usize,
},
ClockSourceUnsupported {
source: ClockSource,
platform: &'static str,
},
CompileTimeArgvForbidden,
CompileTimeConfigInvalid {
reason: &'static str,
},
}
#[cfg(not(feature = "compile-time-config"))]
impl core::fmt::Display for ConfigError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
ConfigError::MissingValue(flag) => write!(f, "{flag} requires a value"),
ConfigError::MissingRequired(flag) => write!(f, "missing required flag {flag}"),
ConfigError::UnknownFlag(s) => write!(f, "unknown flag {s}"),
ConfigError::BadInteger { flag, raw } => {
write!(f, "{flag}: not a valid unsigned integer: {raw:?}")
}
ConfigError::BadSocketMode(raw) => {
write!(
f,
"--socket-mode: expected octal digits (e.g. 600, 0600, or 0o600), got: {raw:?}"
)
}
ConfigError::BadAddr(raw) => {
write!(f, "--prom-addr: not a valid socket address: {raw:?}")
}
ConfigError::BadValue { flag, raw } => {
write!(f, "{flag}: invalid value {raw:?}",)
}
ConfigError::HelpRequested => f.write_str("--help"),
ConfigError::ThresholdTooLow { value, min } => {
write!(
f,
"--threshold-ms: {value} is below the minimum allowed value ({min} ms)"
)
}
ConfigError::MutuallyExclusive { a, b } => {
write!(f, "{a} and {b} are mutually exclusive")
}
ConfigError::RemovedFlag { flag, replacement } => write!(
f,
"{flag} has been removed for security reasons; use {replacement}"
),
ConfigError::PromAddrRequiresToken => f.write_str(
"--prom-addr requires --prom-token-file. /metrics has no anonymous access; \
generate a token with `openssl rand -hex 32 > /etc/varta/prom.token && \
chmod 600 /etc/varta/prom.token`.",
),
ConfigError::ShutdownGraceTooLow { value, min } => write!(
f,
"--shutdown-grace-ms: {value} is below the minimum allowed value ({min} ms)"
),
ConfigError::RecoveryCaptureBytesTooLarge { value, max } => write!(
f,
"--recovery-capture-bytes: {value} exceeds the maximum allowed value ({max} bytes)"
),
ConfigError::RecoveryCaptureRequiresRecovery => f.write_str(
"--recovery-capture-stdio requires --recovery-exec or --recovery-exec-file",
),
ConfigError::ShellRecoveryNotCompiledIn => f.write_str(
"shell-mode recovery has been permanently removed; use --recovery-exec instead",
),
ConfigError::RecoveryRequiresAuthenticatedTransport { udp_addr } => write!(
f,
"recovery command is configured alongside a UDP listener on {udp_addr}. \
UDP transports cannot attest the sending process — a holder of the AEAD key \
(or a per-agent key derived from a leaked master key) can forge a beat \
claiming any pid, then stop sending to trigger recovery against the chosen pid. \
Either remove the recovery command, switch to a UDS-only deployment, or pass \
--secure-udp-i-accept-recovery-on-unauthenticated-transport (for secure UDP) \
or --plaintext-udp-i-accept-recovery-on-unauthenticated-transport (for plaintext \
UDP) to explicitly accept this risk on a per-listener basis."
),
ConfigError::SecureUdpRequiresLoopbackBind { udp_addr } => write!(
f,
"secure-UDP listener configured with non-loopback --udp-bind-addr ({udp_addr}). \
The per-sender replay-state map holds up to 1024 senders plus a 1-deep \
eviction shadow; an attacker who can spoof ≥1025 UDP source addresses can \
rotate the shadow and replay a captured frame against a target sender. \
Either bind to a loopback address (default 127.0.0.1) or pass \
--i-accept-secure-udp-non-loopback to explicitly accept this risk. \
See book/src/architecture/vlp-transports.md for the threat-boundary derivation."
),
ConfigError::IterationBudgetOutOfRange { value, min, max } => write!(
f,
"--iteration-budget-ms: {value} is outside the accepted range [{min}, {max}] ms"
),
ConfigError::ScrapeBudgetOutOfRange { value, min, max } => write!(
f,
"--scrape-budget-ms: {value} is outside the accepted range [{min}, {max}] ms"
),
ConfigError::EvictionScanWindowOutOfRange { value, min, max } => write!(
f,
"--eviction-scan-window: {value} is outside the accepted range [{min}, {max}]"
),
ConfigError::ClockSourceUnsupported { source, platform } => {
let hint = match source {
crate::clock::ClockSource::Boottime => {
"`boottime` semantics (advance through suspend) require Linux's \
CLOCK_BOOTTIME. On macOS / iOS use `--clock-source monotonic-raw` \
(mach_continuous_time) for the same semantics; BSD has no equivalent \
kernel clock."
}
crate::clock::ClockSource::MonotonicRaw => {
"`monotonic-raw` is macOS / iOS only (CLOCK_MONOTONIC_RAW = \
mach_continuous_time). On Linux use `--clock-source boottime` \
(CLOCK_BOOTTIME) for advance-through-suspend semantics; BSD \
has no equivalent kernel clock."
}
crate::clock::ClockSource::Monotonic => "",
};
write!(
f,
"--clock-source {source} is not supported on `{platform}`. {hint} \
Otherwise use `--clock-source monotonic` (the default)."
)
}
ConfigError::CompileTimeArgvForbidden => f.write_str(
"this binary was configured at compile time \
(--features compile-time-config); refusing to accept argv. \
See book/src/architecture/compile-time-config.md for the \
supported configuration mechanism.",
),
ConfigError::CompileTimeConfigInvalid { reason } => write!(
f,
"compile-time config violates a cross-field invariant: {reason}"
),
}
}
}
#[cfg(feature = "compile-time-config")]
impl core::fmt::Display for ConfigError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
const REF: &str = "see book/src/architecture/compile-time-config.md";
match self {
ConfigError::MissingValue(_)
| ConfigError::MissingRequired(_)
| ConfigError::UnknownFlag(_)
| ConfigError::BadInteger { .. }
| ConfigError::BadSocketMode(_)
| ConfigError::BadAddr(_)
| ConfigError::BadValue { .. }
| ConfigError::HelpRequested
| ConfigError::MutuallyExclusive { .. }
| ConfigError::RemovedFlag { .. }
| ConfigError::PromAddrRequiresToken
| ConfigError::ShutdownGraceTooLow { .. }
| ConfigError::RecoveryCaptureBytesTooLarge { .. }
| ConfigError::RecoveryCaptureRequiresRecovery
| ConfigError::RecoveryRequiresAuthenticatedTransport { .. }
| ConfigError::SecureUdpRequiresLoopbackBind { .. }
| ConfigError::IterationBudgetOutOfRange { .. }
| ConfigError::ScrapeBudgetOutOfRange { .. }
| ConfigError::EvictionScanWindowOutOfRange { .. } => {
write!(f, "configuration error (argv path unreachable; {REF})")
}
ConfigError::ThresholdTooLow { value, min } => {
write!(f, "threshold below minimum: {value} ms < {min} ms ({REF})")
}
ConfigError::ShellRecoveryNotCompiledIn => write!(
f,
"shell-mode recovery has been permanently removed ({REF})"
),
ConfigError::ClockSourceUnsupported { platform, .. } => write!(
f,
"configured clock source is not supported on `{platform}`; \
only the monotonic source is available off Linux ({REF})"
),
ConfigError::CompileTimeArgvForbidden => f.write_str(
"this binary was configured at compile time; \
refusing to accept command-line arguments",
),
ConfigError::CompileTimeConfigInvalid { reason } => write!(
f,
"compile-time config violates a cross-field invariant: {reason}"
),
}
}
}
impl std::error::Error for ConfigError {}