Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion netengine/diagnostic/preflight.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."
)


Expand Down
17 changes: 11 additions & 6 deletions netengine/handlers/dns.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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,
Expand All @@ -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"])

Expand Down
24 changes: 24 additions & 0 deletions tests/test_doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down