fix: advisory + exit(1) when port 53 is already in use (#45) (#47)

* 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:
Razvan Dimescu
2026-04-09 15:03:58 +03:00
committed by GitHub
parent 27dfaab360
commit a6f23a5ddb
3 changed files with 197 additions and 1 deletions

View File

@@ -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,