From 6887c8e02e6eec69dc21da8f19e9e406c8bd16aa Mon Sep 17 00:00:00 2001 From: Razvan Dimescu Date: Wed, 8 Apr 2026 01:31:16 +0300 Subject: [PATCH] refactor: move data_dir override from env var to [server] TOML field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts the NUMA_DATA_DIR env var added in the previous commit and replaces it with a [server] data_dir TOML field. Numa already has a well-developed config system; adding a parallel env-var mechanism for a single knob was wrong. The principle: TOML is for application behavior configuration. Env vars are for bootstrap values (HOME, SUDO_USER to discover paths before config loads) and standard ecosystem conventions (RUST_LOG). data_dir is neither — it's an app knob, so it belongs in the TOML. Changes: - lib.rs::data_dir() reverts to the platform-specific fallback only - config.rs adds `data_dir: Option` to ServerConfig - main.rs resolves config.server.data_dir with fallback to numa::data_dir() and passes it to build_tls_config, then stores the resolved path on ctx.data_dir for downstream consumers - tls.rs::build_tls_config takes `data_dir: &Path` as an explicit parameter instead of calling crate::data_dir() behind the caller's back. regenerate_tls and dot.rs self_signed_tls now pass &ctx.data_dir, honoring whatever path the config resolved to - tests/integration.sh Suite 6 uses `data_dir = "$NUMA_DATA"` in its test TOML instead of the NUMA_DATA_DIR env var prefix - numa.toml gains a commented-out data_dir example No behavior change for existing production deployments (the default path is unchanged). Test harness is now fully config-driven, and containerized deploys can override data_dir via mount+config without needing env var injection. 127/127 unit tests pass, Suite 6 passes end-to-end. Co-Authored-By: Claude Opus 4.6 (1M context) --- numa.toml | 5 +++++ src/config.rs | 5 +++++ src/dot.rs | 2 +- src/lib.rs | 9 +++------ src/main.rs | 17 +++++++++++++++-- src/tls.rs | 8 +++++--- tests/integration.sh | 8 +++++--- 7 files changed, 39 insertions(+), 15 deletions(-) diff --git a/numa.toml b/numa.toml index b7f98de..35d92de 100644 --- a/numa.toml +++ b/numa.toml @@ -2,6 +2,11 @@ bind_addr = "0.0.0.0:53" api_port = 5380 # api_bind_addr = "127.0.0.1" # default; set to "0.0.0.0" for LAN dashboard access +# data_dir = "/usr/local/var/numa" # where numa stores TLS CA and cert material + # (default: /usr/local/var/numa on unix, + # %PROGRAMDATA%\numa on windows). Override for + # containerized deploys or tests that can't + # write to the system path. # [upstream] # mode = "forward" # "forward" (default) — relay to upstream diff --git a/src/config.rs b/src/config.rs index acf4d37..45dc896 100644 --- a/src/config.rs +++ b/src/config.rs @@ -41,6 +41,10 @@ pub struct ServerConfig { pub api_port: u16, #[serde(default = "default_api_bind_addr")] pub api_bind_addr: String, + /// Where numa writes TLS material (CA, leaf certs, regenerated state). + /// Defaults to `crate::data_dir()` (platform-specific system path) if unset. + #[serde(default)] + pub data_dir: Option, } impl Default for ServerConfig { @@ -49,6 +53,7 @@ impl Default for ServerConfig { bind_addr: default_bind_addr(), api_port: default_api_port(), api_bind_addr: default_api_bind_addr(), + data_dir: None, } } } diff --git a/src/dot.rs b/src/dot.rs index d399649..a09b160 100644 --- a/src/dot.rs +++ b/src/dot.rs @@ -61,7 +61,7 @@ fn load_tls_config(cert_path: &Path, key_path: &Path) -> crate::Result Option> { let service_names = [ctx.proxy_tld.clone()]; - match crate::tls::build_tls_config(&ctx.proxy_tld, &service_names, dot_alpn()) { + match crate::tls::build_tls_config(&ctx.proxy_tld, &service_names, dot_alpn(), &ctx.data_dir) { Ok(cfg) => Some(cfg), Err(e) => { warn!( diff --git a/src/lib.rs b/src/lib.rs index 05d18a0..347e72f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -66,15 +66,12 @@ fn config_dir_unix() -> std::path::PathBuf { std::path::PathBuf::from("/usr/local/var/numa") } -/// System-wide data directory for TLS certs. -/// Override with `NUMA_DATA_DIR` env var (useful for containerized -/// deployments and integration tests that can't write to the default path). +/// Default system-wide data directory for TLS certs. Overridable via +/// `[server] data_dir = "..."` in numa.toml — this function only provides +/// the fallback when the config doesn't set it. /// Unix: /usr/local/var/numa /// Windows: %PROGRAMDATA%\numa pub fn data_dir() -> std::path::PathBuf { - if let Ok(dir) = std::env::var("NUMA_DATA_DIR") { - return std::path::PathBuf::from(dir); - } #[cfg(windows)] { std::path::PathBuf::from( diff --git a/src/main.rs b/src/main.rs index adf266e..af0fb3a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -204,10 +204,23 @@ async fn main() -> numa::Result<()> { let forwarding_rules = system_dns.forwarding_rules; + // Resolve data_dir from config, falling back to the platform default. + // Used for TLS CA storage below and stored on ServerCtx for runtime use. + let resolved_data_dir = config + .server + .data_dir + .clone() + .unwrap_or_else(numa::data_dir); + // Build initial TLS config before ServerCtx (so ArcSwap is ready at construction) let initial_tls = if config.proxy.enabled && config.proxy.tls_port > 0 { let service_names = service_store.names(); - match numa::tls::build_tls_config(&config.proxy.tld, &service_names, Vec::new()) { + match numa::tls::build_tls_config( + &config.proxy.tld, + &service_names, + Vec::new(), + &resolved_data_dir, + ) { Ok(tls_config) => Some(ArcSwap::from(tls_config)), Err(e) => { log::warn!("TLS setup failed, HTTPS proxy disabled: {}", e); @@ -248,7 +261,7 @@ async fn main() -> numa::Result<()> { config_path: resolved_config_path, config_found, config_dir: numa::config_dir(), - data_dir: numa::data_dir(), + data_dir: resolved_data_dir, tls_config: initial_tls, upstream_mode: resolved_mode, root_hints, diff --git a/src/tls.rs b/src/tls.rs index 5746f3b..c60714e 100644 --- a/src/tls.rs +++ b/src/tls.rs @@ -24,7 +24,7 @@ pub fn regenerate_tls(ctx: &ServerCtx) { names.extend(ctx.lan_peers.lock().unwrap().names()); let names: Vec = names.into_iter().collect(); - match build_tls_config(&ctx.proxy_tld, &names, Vec::new()) { + match build_tls_config(&ctx.proxy_tld, &names, Vec::new(), &ctx.data_dir) { Ok(new_config) => { tls.store(new_config); info!("TLS cert regenerated for {} services", names.len()); @@ -38,13 +38,15 @@ pub fn regenerate_tls(ctx: &ServerCtx) { /// so we list each service explicitly as a SAN. /// `alpn` is advertised in the TLS ServerHello — pass empty for the proxy /// (which accepts any ALPN), or `[b"dot"]` for DoT (RFC 7858 §3.2). +/// `data_dir` is where the CA material is stored — taken from +/// `[server] data_dir` in numa.toml (defaults to `crate::data_dir()`). pub fn build_tls_config( tld: &str, service_names: &[String], alpn: Vec>, + data_dir: &Path, ) -> crate::Result> { - let dir = crate::data_dir(); - let (ca_cert, ca_key) = ensure_ca(&dir)?; + let (ca_cert, ca_key) = ensure_ca(data_dir)?; let (cert_chain, key) = generate_service_cert(&ca_cert, &ca_key, tld, service_names)?; // Ensure a crypto provider is installed (rustls needs one) diff --git a/tests/integration.sh b/tests/integration.sh index f1c5205..473356e 100755 --- a/tests/integration.sh +++ b/tests/integration.sh @@ -542,8 +542,9 @@ else PROXY_HTTPS_PORT=8443 NUMA_DATA=/tmp/numa-integration-data - # Fresh data dir so we generate a fresh CA for this suite — NUMA_DATA_DIR - # env var lets numa write under $TMPDIR instead of /usr/local/var/numa. + # Fresh data dir so we generate a fresh CA for this suite. Path is set + # via [server] data_dir in the TOML below, not an env var — numa treats + # its config file as the single source of truth for all knobs. rm -rf "$NUMA_DATA" mkdir -p "$NUMA_DATA" @@ -551,6 +552,7 @@ else [server] bind_addr = "127.0.0.1:$PORT" api_port = $API_PORT +data_dir = "$NUMA_DATA" [upstream] mode = "forward" @@ -582,7 +584,7 @@ value = "10.0.0.1" ttl = 60 CONF - NUMA_DATA_DIR="$NUMA_DATA" RUST_LOG=info "$BINARY" "$CONFIG" > "$LOG" 2>&1 & + RUST_LOG=info "$BINARY" "$CONFIG" > "$LOG" 2>&1 & NUMA_PID=$! sleep 4