Merge branch 'razvandimescu:main' into feat/add-arch-linux-support
This commit is contained in:
19
src/main.rs
19
src/main.rs
@@ -223,7 +223,11 @@ async fn main() -> numa::Result<()> {
|
||||
) {
|
||||
Ok(tls_config) => Some(ArcSwap::from(tls_config)),
|
||||
Err(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
|
||||
}
|
||||
}
|
||||
@@ -231,8 +235,21 @@ async fn main() -> numa::Result<()> {
|
||||
None
|
||||
};
|
||||
|
||||
let socket = match UdpSocket::bind(&config.server.bind_addr).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
if let Some(advisory) =
|
||||
numa::system_dns::try_port53_advisory(&config.server.bind_addr, &e)
|
||||
{
|
||||
eprint!("{}", advisory);
|
||||
std::process::exit(1);
|
||||
}
|
||||
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,60 @@ pub fn discover_system_dns() -> SystemDnsInfo {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
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";
|
||||
Some(format!(
|
||||
"
|
||||
{o}Numa{r} — cannot bind to {bind_addr}: {title}.
|
||||
|
||||
{cause}
|
||||
|
||||
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
|
||||
|
||||
"
|
||||
))
|
||||
}
|
||||
|
||||
fn is_port_53(bind_addr: &str) -> bool {
|
||||
bind_addr
|
||||
.parse::<SocketAddr>()
|
||||
.map(|s| s.port() == 53)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn discover_macos() -> SystemDnsInfo {
|
||||
use log::{debug, warn};
|
||||
@@ -1753,4 +1807,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());
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
138
tests/docker/smoke-port53.sh
Executable file
138
tests/docker/smoke-port53.sh
Executable file
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Port-53 conflict advisory integration test.
|
||||
#
|
||||
# Builds numa from source inside a debian:bookworm container, pre-binds
|
||||
# port 53 with a UDP socket, then runs numa bare (default bind_addr
|
||||
# 0.0.0.0:53). Verifies:
|
||||
# - process exits with code 1
|
||||
# - stderr contains the advisory ("cannot bind to")
|
||||
# - stderr contains both fix suggestions ("numa install", "bind_addr")
|
||||
#
|
||||
# This is the end-to-end test for the fix in:
|
||||
# src/main.rs — AddrInUse match arm → eprint advisory + process::exit(1)
|
||||
#
|
||||
# No systemd-resolved needed — the conflict is simulated by a Python
|
||||
# UDP socket held open before numa starts.
|
||||
#
|
||||
# Requirements: docker
|
||||
# Usage: ./tests/docker/smoke-port53.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
GREEN="\033[32m"; RED="\033[31m"; RESET="\033[0m"
|
||||
|
||||
pass() { printf " ${GREEN}✓${RESET} %s\n" "$1"; }
|
||||
fail() { printf " ${RED}✗${RESET} %s\n" "$1"; printf " %s\n" "$2"; FAILED=$((FAILED+1)); }
|
||||
FAILED=0
|
||||
|
||||
echo "── smoke-port53: building + testing numa on debian:bookworm ──"
|
||||
echo " (first run is slow: image pull + cold cargo build, ~5-8 min)"
|
||||
echo
|
||||
|
||||
OUTPUT=$(docker run --rm \
|
||||
--platform linux/amd64 \
|
||||
-v "$PWD:/src:ro" \
|
||||
-v numa-port53-cargo:/root/.cargo \
|
||||
-v numa-port53-target:/work/target \
|
||||
debian:bookworm bash -c '
|
||||
set -e
|
||||
|
||||
apt-get update -qq && apt-get install -y -qq curl build-essential python3 2>&1 | tail -3
|
||||
|
||||
# Install rustup if not already in the cargo cache volume
|
||||
if ! command -v cargo &>/dev/null; then
|
||||
curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --quiet
|
||||
fi
|
||||
. "$HOME/.cargo/env"
|
||||
|
||||
# Copy source to a writable workdir
|
||||
mkdir -p /work
|
||||
tar -C /src --exclude=./target --exclude=./.git -cf - . | tar -C /work -xf -
|
||||
cd /work
|
||||
|
||||
echo "── cargo build --release --locked ──"
|
||||
cargo build --release --locked 2>&1 | tail -5
|
||||
echo
|
||||
|
||||
# Write the holder script to a file to avoid quoting hell.
|
||||
# Holds port 53 until killed — no sleep race.
|
||||
cat > /tmp/hold53.py << '"'"'PYEOF'"'"'
|
||||
import socket, signal
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 0)
|
||||
s.bind(("", 53))
|
||||
signal.pause()
|
||||
PYEOF
|
||||
|
||||
python3 /tmp/hold53.py &
|
||||
HOLDER_PID=$!
|
||||
|
||||
# Verify the holder is actually up before proceeding
|
||||
sleep 0.3
|
||||
if ! kill -0 $HOLDER_PID 2>/dev/null; then
|
||||
echo "holder_failed=1"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "── running numa with port 53 already bound ──"
|
||||
# timeout 5: guards against numa not exiting (advisory not fired, bug present)
|
||||
# Capture stderr to a file so the exit code is not clobbered by || or $()
|
||||
set +e
|
||||
timeout 5 ./target/release/numa > /tmp/numa-stderr.txt 2>&1
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
STDERR=$(cat /tmp/numa-stderr.txt)
|
||||
|
||||
kill $HOLDER_PID 2>/dev/null || true
|
||||
|
||||
echo "exit_code=$EXIT_CODE"
|
||||
printf "%s" "$STDERR" | sed "s/^/ numa: /"
|
||||
' 2>&1)
|
||||
|
||||
echo "$OUTPUT"
|
||||
|
||||
echo
|
||||
echo "── assertions ──"
|
||||
|
||||
if echo "$OUTPUT" | grep -q "holder_failed=1"; then
|
||||
echo " SETUP FAILED: could not pre-bind port 53 inside container"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
EXIT_CODE=$(echo "$OUTPUT" | grep '^exit_code=' | cut -d= -f2)
|
||||
|
||||
if [ "${EXIT_CODE:-}" = "1" ]; then
|
||||
pass "exits with code 1"
|
||||
else
|
||||
fail "exits with code 1" "got: exit_code=${EXIT_CODE:-<missing>}"
|
||||
fi
|
||||
|
||||
if echo "$OUTPUT" | grep -q "cannot bind to"; then
|
||||
pass "advisory printed to stderr"
|
||||
else
|
||||
fail "advisory printed to stderr" "stderr did not contain 'cannot bind to'"
|
||||
fi
|
||||
|
||||
if echo "$OUTPUT" | grep -q "numa install"; then
|
||||
pass "advisory offers 'sudo numa install'"
|
||||
else
|
||||
fail "advisory offers 'sudo numa install'" "not found in output"
|
||||
fi
|
||||
|
||||
if echo "$OUTPUT" | grep -q "bind_addr"; then
|
||||
pass "advisory offers non-privileged port alternative"
|
||||
else
|
||||
fail "advisory offers non-privileged port alternative" "'bind_addr' not found in output"
|
||||
fi
|
||||
|
||||
echo
|
||||
if [ "$FAILED" -eq 0 ]; then
|
||||
printf "${GREEN}── smoke-port53 passed ──${RESET}\n"
|
||||
exit 0
|
||||
else
|
||||
printf "${RED}── smoke-port53 failed ($FAILED assertion(s)) ──${RESET}\n"
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user