* fix: advisory + exit(1) when port 53 is already in use (#45) Detect AddrInUse on bind, print a human-readable diagnostic explaining systemd-resolved / Dnscache as the likely cause and offer two concrete fixes (sudo numa install, or bind_addr on a non-privileged port), then exit(1) instead of surfacing a raw OS error. Adds tests/docker/smoke-port53.sh: end-to-end Docker test that pre-binds port 53 with a Python UDP socket and asserts the advisory + exit code. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: collapse port53 advisory to single flat path The per-platform cause sentences were cosmetic — they didn't change the user's actions (install, or bind_addr on a non-privileged port), but they introduced duplicated "another process..." strings, a dead-from-CI branch (is_systemd_resolved_active() == true is never reached by any test), and a pub visibility bump on is_systemd_resolved_active for a single caller. Replace with one flat format! whose cause line mentions both systemd-resolved and the Windows DNS Client inline. The existing smoke test now exercises 100% of the function. is_systemd_resolved_active reverts to private. 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 #47.
This commit is contained in:
17
src/main.rs
17
src/main.rs
@@ -231,8 +231,23 @@ async fn main() -> numa::Result<()> {
|
||||
None
|
||||
};
|
||||
|
||||
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) =>
|
||||
{
|
||||
eprint!(
|
||||
"{}",
|
||||
numa::system_dns::port53_conflict_advisory(&config.server.bind_addr)
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
|
||||
let ctx = Arc::new(ServerCtx {
|
||||
socket: UdpSocket::bind(&config.server.bind_addr).await?,
|
||||
socket,
|
||||
zone_map: build_zone_map(&config.zones)?,
|
||||
cache: RwLock::new(DnsCache::new(
|
||||
config.cache.max_entries,
|
||||
|
||||
@@ -46,6 +46,49 @@ 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)
|
||||
}
|
||||
|
||||
/// 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 o = "\x1b[1;38;2;192;98;58m"; // bold orange
|
||||
let r = "\x1b[0m";
|
||||
format!(
|
||||
"
|
||||
{o}Numa{r} — cannot bind to {bind_addr}: port 53 is already in use.
|
||||
|
||||
Another process is already bound to port 53. On Linux this is
|
||||
typically systemd-resolved; on Windows, the DNS Client service.
|
||||
|
||||
Fix — pick one:
|
||||
|
||||
1. Install Numa as the system resolver (frees port 53):
|
||||
|
||||
sudo numa install (on Windows, run as Administrator)
|
||||
|
||||
2. Run on a non-privileged port for testing.
|
||||
Create ~/.config/numa/numa.toml with:
|
||||
|
||||
[server]
|
||||
bind_addr = \"127.0.0.1:5354\"
|
||||
api_port = 5380
|
||||
|
||||
Then run: numa
|
||||
Test with: dig @127.0.0.1 -p 5354 example.com
|
||||
|
||||
"
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn discover_macos() -> SystemDnsInfo {
|
||||
use log::{debug, warn};
|
||||
|
||||
Reference in New Issue
Block a user