fix: human-readable advisories for TLS data_dir + port-53 EACCES #48

Merged
razvandimescu merged 3 commits from fix/tls-permission-advisory into main 2026-04-09 21:27:08 +08:00
2 changed files with 80 additions and 29 deletions
Showing only changes of commit 10024161aa - Show all commits

View File

@@ -241,17 +241,15 @@ async fn main() -> numa::Result<()> {
let socket = match UdpSocket::bind(&config.server.bind_addr).await {
Ok(s) => s,
Err(e)
if e.kind() == std::io::ErrorKind::AddrInUse
&& numa::system_dns::is_port_53(&config.server.bind_addr) =>
Err(e) => {
if let Some(advisory) =
numa::system_dns::try_port53_advisory(&config.server.bind_addr, &e)
{
eprint!(
"{}",
numa::system_dns::port53_conflict_advisory(&config.server.bind_addr)
);
eprint!("{}", advisory);
std::process::exit(1);
}
Err(e) => return Err(e.into()),
return Err(e.into());
}
};
let ctx = Arc::new(ServerCtx {

View File

@@ -46,28 +46,35 @@ pub fn discover_system_dns() -> SystemDnsInfo {
}
}
/// True if `bind_addr` targets DNS port 53. Used to scope the port-53
/// conflict advisory — we only want to print the systemd-resolved /
/// Dnscache hint when the user is actually trying to bind the DNS port.
pub fn is_port_53(bind_addr: &str) -> bool {
bind_addr
.parse::<SocketAddr>()
.map(|s| s.port() == 53)
.unwrap_or(false)
/// 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.
pub fn try_port53_advisory(bind_addr: &str, err: &std::io::Error) -> Option<String> {
if !is_port_53(bind_addr) {
return None;
}
/// Human-readable diagnostic for port-53 bind conflicts. Offers two
/// concrete fixes: install Numa as the system resolver, or bind to a
/// non-privileged port.
pub fn port53_conflict_advisory(bind_addr: &str) -> String {
let (title, cause) = match err.kind() {
std::io::ErrorKind::AddrInUse => (
"port 53 is already in use",
"Another process is already bound to port 53. On Linux this is\n \
typically systemd-resolved; on Windows, the DNS Client service.",
),
std::io::ErrorKind::PermissionDenied => (
"permission denied",
"Port 53 is privileged — binding it requires root on Linux/macOS\n \
or Administrator on Windows.",
),
_ => return None,
};
let o = "\x1b[1;38;2;192;98;58m"; // bold orange
let r = "\x1b[0m";
format!(
Some(format!(
"
{o}Numa{r} — cannot bind to {bind_addr}: port 53 is already in use.
{o}Numa{r} — cannot bind to {bind_addr}: {title}.
Another process is already bound to port 53. On Linux this is
typically systemd-resolved; on Windows, the DNS Client service.
{cause}
Fix — pick one:
@@ -86,7 +93,14 @@ pub fn port53_conflict_advisory(bind_addr: &str) -> String {
Test with: dig @127.0.0.1 -p 5354 example.com
"
)
))
}
fn is_port_53(bind_addr: &str) -> bool {
bind_addr
.parse::<SocketAddr>()
.map(|s| s.port() == 53)
.unwrap_or(false)
}
#[cfg(target_os = "macos")]
@@ -1796,4 +1810,43 @@ Wireless LAN adapter Wi-Fi:
assert_eq!(result.len(), 1);
assert!(result.contains_key("Wi-Fi"));
}
#[test]
fn try_port53_advisory_addr_in_use() {
let err = std::io::Error::from(std::io::ErrorKind::AddrInUse);
let msg = try_port53_advisory("0.0.0.0:53", &err).expect("should advise on port 53");
assert!(msg.contains("cannot bind to"));
assert!(msg.contains("already in use"));
assert!(msg.contains("numa install"));
assert!(msg.contains("bind_addr"));
}
#[test]
fn try_port53_advisory_permission_denied() {
let err = std::io::Error::from(std::io::ErrorKind::PermissionDenied);
let msg = try_port53_advisory("0.0.0.0:53", &err).expect("should advise on port 53");
assert!(msg.contains("cannot bind to"));
assert!(msg.contains("permission denied"));
assert!(msg.contains("numa install"));
assert!(msg.contains("bind_addr"));
}
#[test]
fn try_port53_advisory_skips_non_53_ports() {
let err = std::io::Error::from(std::io::ErrorKind::AddrInUse);
assert!(try_port53_advisory("127.0.0.1:5354", &err).is_none());
assert!(try_port53_advisory("[::]:853", &err).is_none());
}
#[test]
fn try_port53_advisory_skips_unrelated_error_kinds() {
let err = std::io::Error::from(std::io::ErrorKind::NotFound);
assert!(try_port53_advisory("0.0.0.0:53", &err).is_none());
}
#[test]
fn try_port53_advisory_skips_malformed_bind_addr() {
let err = std::io::Error::from(std::io::ErrorKind::AddrInUse);
assert!(try_port53_advisory("not-an-address", &err).is_none());
}
}