fix: human-readable advisories for TLS data_dir + port-53 EACCES (#48)
* fix: human-readable advisory when TLS data_dir is not writable When numa runs as non-root on a system with a privileged default data_dir (e.g. /usr/local/var/numa on macOS), TLS CA setup fails with a raw "Permission denied (os error 13)" and HTTPS proxy is silently disabled. The user sees a cryptic warning with no path forward. Detect std::io::ErrorKind::PermissionDenied on the tls error, print a diagnostic naming the data_dir and offering two fixes (install as system resolver, or point data_dir at a writable path), and keep the graceful-degradation behavior — DNS resolution and plain-HTTP proxy continue to work without HTTPS. All other TLS setup errors fall through to the existing log::warn!. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: port-53 advisory also handles EACCES (non-root privileged bind) The original port-53 match arm only caught EADDRINUSE, so a fresh non-root user on macOS/Linux hitting EACCES when trying to bind a privileged port saw the raw OS error instead of the advisory. Collapse the scoping helper and the advisory into a single `try_port53_advisory(bind_addr, &io::Error) -> Option<String>` that returns the formatted diagnostic when both the port is 53 and the error kind is one we can speak to (AddrInUse or PermissionDenied), and `None` otherwise. The two failure modes share one body with a cause-sentence variant — no duplicated fix text. Caller becomes a plain if-let: no match guard, no separate is_port_53 helper exposed on the public API. is_port_53 goes back to private. Unit tests cover all branches: AddrInUse, PermissionDenied, non-53 bind_addr, unrelated ErrorKind, and malformed bind_addr. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * 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> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit was merged in pull request #48.
This commit is contained in:
64
src/tls.rs
64
src/tls.rs
@@ -40,6 +40,40 @@ pub fn regenerate_tls(ctx: &ServerCtx) {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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";
|
||||
Some(format!(
|
||||
"
|
||||
{o}Numa{r} — HTTPS proxy disabled: cannot write TLS CA to {}.
|
||||
|
||||
The data directory is not writable by the current user. Numa needs
|
||||
to persist a local Certificate Authority there to serve .numa over
|
||||
HTTPS. DNS resolution and plain-HTTP proxy continue to work.
|
||||
|
||||
Fix — pick one:
|
||||
|
||||
1. Install Numa as the system resolver (sets up a writable data dir):
|
||||
|
||||
sudo numa install (on Windows, run as Administrator)
|
||||
|
||||
2. Point data_dir at a path you can write.
|
||||
Create ~/.config/numa/numa.toml with:
|
||||
|
||||
[server]
|
||||
data_dir = \"/path/you/can/write\"
|
||||
|
||||
",
|
||||
data_dir.display()
|
||||
))
|
||||
}
|
||||
|
||||
/// Build a TLS config with a cert covering all provided service names.
|
||||
/// Wildcards under single-label TLDs (*.numa) are rejected by browsers,
|
||||
/// so we list each service explicitly as a SAN.
|
||||
@@ -170,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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user