From 168ad42994b662f6d5f0c8fb9cd8e0cd201eaa6b Mon Sep 17 00:00:00 2001 From: aidankhogg Date: Mon, 29 Jun 2026 22:57:33 +0100 Subject: [PATCH] Align DNS doctor ports with CoreDNS bindings --- netengine/diagnostic/preflight.py | 4 +++- netengine/handlers/dns.py | 17 +++++++++++------ tests/test_doctor.py | 24 ++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/netengine/diagnostic/preflight.py b/netengine/diagnostic/preflight.py index 6c1c3eb..b93e750 100644 --- a/netengine/diagnostic/preflight.py +++ b/netengine/diagnostic/preflight.py @@ -107,7 +107,9 @@ class KnownPort(NamedTuple): _DNS_PORT_HINT = ( "Port 53 is commonly held by a local DNS resolver. Stop or reconfigure systemd-resolved, " - "dnsmasq, named/CoreDNS, or another DNS server, or change the NetEngine DNS bind port." + "dnsmasq, named/CoreDNS, or another DNS server. On macOS with Docker Desktop, " + "binding privileged host port 53 may require elevated privileges or a configurable " + "alternate DNS host port." ) diff --git a/netengine/handlers/dns.py b/netengine/handlers/dns.py index c23eabd..f48ef3b 100644 --- a/netengine/handlers/dns.py +++ b/netengine/handlers/dns.py @@ -24,6 +24,11 @@ COREDNS_IMAGE = "coredns/coredns:1.11.3" COREDNS_CONTAINER_NAME = "netengine_coredns" +COREDNS_PORT_BINDINGS = { + "53/udp": ("127.0.0.1", 53), + "53/tcp": ("127.0.0.1", 53), +} +COREDNS_PORTS = tuple(COREDNS_PORT_BINDINGS) class DNSHandler(BasePhaseHandler): @@ -473,11 +478,11 @@ def _sync() -> str: {"core": client.api.create_endpoint_config(ipv4_address=root_listen_ip)} ) - # Expose port 53/udp to host for DNS queries from the orchestrator. - # This allows the host to verify DNS is working even when not on the core network. - port_bindings = { - "53/udp": ("127.0.0.1", 53), - } + # Expose DNS over both UDP and TCP to the host. UDP carries the common query + # path, while TCP is required by DNS for truncation fallback and zone-sized + # responses. This also keeps doctor's required DNS port checks aligned with + # the runtime CoreDNS bindings. + port_bindings = COREDNS_PORT_BINDINGS response = client.api.create_container( image=COREDNS_IMAGE, @@ -489,7 +494,7 @@ def _sync() -> str: port_bindings=port_bindings, ), networking_config=networking_config, - ports=["53/udp"], + ports=list(COREDNS_PORTS), ) client.api.start(response["Id"]) diff --git a/tests/test_doctor.py b/tests/test_doctor.py index a57cbc7..be23f6c 100644 --- a/tests/test_doctor.py +++ b/tests/test_doctor.py @@ -258,6 +258,30 @@ def test_compose_inventory_includes_known_alpha_ports_and_resources() -> None: assert "postgres_data" in volumes +def test_required_dns_ports_match_coredns_port_bindings() -> None: + from netengine.handlers.dns import COREDNS_PORT_BINDINGS + + required_dns_ports = { + (port.port, port.proto) + for port in doctor_mod._preflight.KNOWN_LOCAL_PORTS + if port.label == "DNS" + } + coredns_host_bindings = { + (host_port, container_port.rsplit("/", 1)[1]) + for container_port, (_host_ip, host_port) in COREDNS_PORT_BINDINGS.items() + } + + assert required_dns_ports == coredns_host_bindings + + +def test_port_53_hint_mentions_macos_docker_desktop_and_privileged_binding() -> None: + hint = doctor_mod._preflight._DNS_PORT_HINT + + assert "macOS" in hint + assert "Docker Desktop" in hint + assert "privileged host port 53" in hint + assert "alternate DNS host port" in hint + def test_dns_udp_bind_failure_has_local_resolver_hint(monkeypatch) -> None: def fake_can_bind(port: int, proto: str) -> bool: assert (port, proto) == (53, "udp")