Symptom
With a search list in resolv.conf, every stub resolver asks <name>.<search> before <name> when the name has fewer dots than ndots. Under enforce with query filtering, the gate REFUSES that expanded form unless a rule happens to cover it. glibc and Go treat REFUSED as "try the next form"; c-ares does not — a REFUSED on a multi-label attempt ends its search (src/lib/ares_search.c, search_callback: the issue #852 carve-out for REFUSED/SERVFAIL applies to single-label attempts only). So on such a host:
- a single-label allow rule
myhost does not make myhost resolvable for c-ares clients — the bare name is never tried;
- audit reports a
dns_blocked for myhost.<search>, a name the operator never asked about, for every lookup.
Affected: Node dns.resolve* (not dns.lookup), Python grpcio, curl and gRPC builds on c-ares.
Reproduced on the #126 branch before its synthetic-specific handling, Lima VM, enforce + filtering, search lan:
c-ares (pycares) _gateway: error=6 (DNS server refused query)
glibc (getent) _gateway: 192.168.5.2 ← same second
proxy log 7× "DNS query blocked (domain not allowed)" domain=_gateway.lan
#128 closes this for the synthetic names by answering <synthetic>.<host search suffix> NXDOMAIN locally — correct there because the bare name is what resolves. This issue is the general form, which needs the opposite treatment: for a real host the expanded form is what resolves.
Why the gate does what it does
The gate judges the name as asked, after stripping the configured search domains — the operator's search-domains plus the Kubernetes defaults (StripSearchDomains). The host's own resolv.conf search list is never consulted, so myhost.lan is not myhost for matching purposes, and a denied query is REFUSED. REFUSED is the honest answer for a name policy denies; it is the wrong rcode for a form the client is merely iterating through on its way to a name policy allows.
Proposal
Strip the host's own resolv.conf search suffixes before the gate judges a name — the Kubernetes-suffix semantics StripSearchDomains already has — so myservice.corp.lan is matched as myservice, forwarded when allowed, and the client gets the real answer on its first attempt. Stripping only: not the search-domain bypass (HasSearchDomainSuffix), which lets unmatched names through and must stay operator-configured, since resolv.conf is DHCP-written.
Not NXDOMAIN. An earlier draft of this issue proposed answering the expanded form NXDOMAIN so the client falls through to the bare name; that is wrong for a real host. On a network with a search list the expanded form is the name that exists — myservice.corp.lan is the record, myservice is a convenience — so NXDOMAINing it and falling to bare myservice breaks the ordinary enterprise short-name pattern instead of fixing it. NXDOMAIN is right only for the resolved synthetic set (#128), whose expanded form never exists and whose bare form is what resolves.
Also not proposed: NXDOMAIN or any other rcode change for names denied outright. REFUSED stays the diagnosable answer for a name policy denies.
Out of scope
- Names whose bare form is also denied — REFUSED stays.
- Container listeners — a container carries its own
resolv.conf.
Symptom
With a search list in
resolv.conf, every stub resolver asks<name>.<search>before<name>when the name has fewer dots thanndots. Under enforce with query filtering, the gate REFUSES that expanded form unless a rule happens to cover it. glibc and Go treat REFUSED as "try the next form"; c-ares does not — a REFUSED on a multi-label attempt ends its search (src/lib/ares_search.c,search_callback: the issue #852 carve-out for REFUSED/SERVFAIL applies to single-label attempts only). So on such a host:myhostdoes not makemyhostresolvable for c-ares clients — the bare name is never tried;dns_blockedformyhost.<search>, a name the operator never asked about, for every lookup.Affected: Node
dns.resolve*(notdns.lookup), Pythongrpcio, curl and gRPC builds on c-ares.Reproduced on the #126 branch before its synthetic-specific handling, Lima VM, enforce + filtering,
search lan:#128 closes this for the synthetic names by answering
<synthetic>.<host search suffix>NXDOMAIN locally — correct there because the bare name is what resolves. This issue is the general form, which needs the opposite treatment: for a real host the expanded form is what resolves.Why the gate does what it does
The gate judges the name as asked, after stripping the configured search domains — the operator's
search-domainsplus the Kubernetes defaults (StripSearchDomains). The host's ownresolv.confsearch list is never consulted, somyhost.lanis notmyhostfor matching purposes, and a denied query is REFUSED. REFUSED is the honest answer for a name policy denies; it is the wrong rcode for a form the client is merely iterating through on its way to a name policy allows.Proposal
Strip the host's own
resolv.confsearch suffixes before the gate judges a name — the Kubernetes-suffix semanticsStripSearchDomainsalready has — somyservice.corp.lanis matched asmyservice, forwarded when allowed, and the client gets the real answer on its first attempt. Stripping only: not the search-domain bypass (HasSearchDomainSuffix), which lets unmatched names through and must stay operator-configured, sinceresolv.confis DHCP-written.Not NXDOMAIN. An earlier draft of this issue proposed answering the expanded form NXDOMAIN so the client falls through to the bare name; that is wrong for a real host. On a network with a search list the expanded form is the name that exists —
myservice.corp.lanis the record,myserviceis a convenience — so NXDOMAINing it and falling to baremyservicebreaks the ordinary enterprise short-name pattern instead of fixing it. NXDOMAIN is right only for the resolved synthetic set (#128), whose expanded form never exists and whose bare form is what resolves.Also not proposed: NXDOMAIN or any other rcode change for names denied outright. REFUSED stays the diagnosable answer for a name policy denies.
Out of scope
resolv.conf.