refactor: move TLS error classification into tls module

main.rs no longer downcasts a boxed error to figure out whether it's
a permission-denied case. tls::try_data_dir_advisory(&err, &dir)
encapsulates the downcast + kind match and returns Some(advisory) or
None, mirroring system_dns::try_port53_advisory. main.rs becomes a
plain if-let, symmetric with the port-53 path.

Trim the docstrings on both advisory functions: they were narrating
the implementation (errno mapping) instead of stating the contract.

Add unit tests for try_data_dir_advisory covering PermissionDenied,
other io::ErrorKind, and non-io errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Razvan Dimescu
2026-04-09 15:32:29 +03:00
parent 10024161aa
commit 5a3b7b1420
3 changed files with 46 additions and 22 deletions

View File

@@ -223,14 +223,10 @@ async fn main() -> numa::Result<()> {
) {
Ok(tls_config) => Some(ArcSwap::from(tls_config)),
Err(e) => {
match e.downcast_ref::<std::io::Error>() {
Some(io_err) if io_err.kind() == std::io::ErrorKind::PermissionDenied => {
eprint!(
"{}",
numa::tls::data_dir_permission_advisory(&resolved_data_dir)
);
}
_ => log::warn!("TLS setup failed, HTTPS proxy disabled: {}", e),
if let Some(advisory) = numa::tls::try_data_dir_advisory(&e, &resolved_data_dir) {
eprint!("{}", advisory);
} else {
log::warn!("TLS setup failed, HTTPS proxy disabled: {}", e);
}
None
}

View File

@@ -46,11 +46,8 @@ pub fn discover_system_dns() -> SystemDnsInfo {
}
}
/// Diagnostic advisory for port-53 bind failures. Returns `Some(msg)`
/// when `bind_addr` targets port 53 and `err` is a kind we can advise
/// on (EADDRINUSE — another process holds it; EACCES — non-root on a
/// privileged port). Returns `None` for non-53 targets or unrelated
/// error kinds, so the caller can fall back to the raw error.
/// Advisory for port-53 bind failures (EADDRINUSE or EACCES); `None`
/// if not applicable so the caller can fall back to the raw error.
pub fn try_port53_advisory(bind_addr: &str, err: &std::io::Error) -> Option<String> {
if !is_port_53(bind_addr) {
return None;

View File

@@ -40,15 +40,16 @@ pub fn regenerate_tls(ctx: &ServerCtx) {
}
}
/// Human-readable diagnostic for TLS data-dir permission failures.
/// Triggered when numa can't write its local CA to the configured
/// data dir (typically `/usr/local/var/numa` without root). HTTPS
/// proxy is disabled; DNS resolution and plain-HTTP proxy keep
/// working.
pub fn data_dir_permission_advisory(data_dir: &Path) -> String {
let o = "\x1b[1;38;2;192;98;58m"; // bold orange
/// Advisory for TLS-setup failures caused by a non-writable data dir;
/// `None` if not applicable so the caller can fall back to the raw error.
pub fn try_data_dir_advisory(err: &crate::Error, data_dir: &Path) -> Option<String> {
let io_err = err.downcast_ref::<std::io::Error>()?;
if io_err.kind() != std::io::ErrorKind::PermissionDenied {
return None;
}
let o = "\x1b[1;38;2;192;98;58m";
let r = "\x1b[0m";
format!(
Some(format!(
"
{o}Numa{r} — HTTPS proxy disabled: cannot write TLS CA to {}.
@@ -70,7 +71,7 @@ pub fn data_dir_permission_advisory(data_dir: &Path) -> String {
",
data_dir.display()
)
))
}
/// Build a TLS config with a cert covering all provided service names.
@@ -203,3 +204,33 @@ fn generate_service_cert(
Ok((vec![cert_der, ca_der], key_der))
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn try_data_dir_advisory_permission_denied() {
let err: crate::Error =
Box::new(std::io::Error::from(std::io::ErrorKind::PermissionDenied));
let path = PathBuf::from("/usr/local/var/numa");
let msg = try_data_dir_advisory(&err, &path).expect("should advise");
assert!(msg.contains("HTTPS proxy disabled"));
assert!(msg.contains("/usr/local/var/numa"));
assert!(msg.contains("numa install"));
assert!(msg.contains("data_dir"));
}
#[test]
fn try_data_dir_advisory_skips_other_io_kinds() {
let err: crate::Error = Box::new(std::io::Error::from(std::io::ErrorKind::NotFound));
assert!(try_data_dir_advisory(&err, &PathBuf::from("/x")).is_none());
}
#[test]
fn try_data_dir_advisory_skips_non_io_errors() {
let err: crate::Error = "rcgen failure".into();
assert!(try_data_dir_advisory(&err, &PathBuf::from("/x")).is_none());
}
}