Skip to content

Provide a DNS resolution hook so the host application's resolver is used #45

Description

@shreemaan-abhishek

Requested by an APISIX committer while reviewing the AI-plugin cutover: apache/apisix#13778 (comment).

Problem

The client resolves hostnames through nginx's resolver directive and nothing else. A host application that owns its own resolver therefore cannot make this client agree with the rest of its outbound traffic.

In APISIX that gap is total. apisix/patch.lua wraps cosocket connect so every hostname goes through core.resolver:

elseif not ipmatcher.parse_ipv4(host) and not ipmatcher.parse_ipv6(host) then
    host, err = resolver.parse_domain(host)

core.resolver is a Lua DNS client (resty.dns.client) that reads /etc/hosts, honours the dns_resolver config, the resolv.conf search domains and SRV records, and keeps its own cache. Every cosocket-based client inherits that. This client dials from C, never touches a cosocket, and so sees only the resolver directive.

Concretely, with APISIX's test resolver (resolver 8.8.8.8 114.114.114.114) every localhost upstream fails:

connect: localhost could not be resolved (3: Host not found)

while lua-resty-http on the same route succeeds, because the name never reaches nginx's resolver.

Why a hook rather than reading the config

This is not the same shape as the trust-store fix in #43. There, the effective value lived in ngx_lua's loc conf and could be read from C. Here the resolver is a Lua module whose lookup yields, so C cannot call it synchronously. The resolution has to happen on the Lua side, before entering the FFI boundary — which also makes this cheap: a binding change, no C change.

Proposed shape

A resolver the host application installs once:

local client = require("resty.ngx_http_ffi_client")

client.set_resolver(function (host)
    return ip, err     -- called only for non-IP hosts
end)

APISIX would wire it once at init (core.resolver.parse_domain) and every plugin moving off lua-resty-http would then get parity for free. A per-call opts.resolver would also work, but the module-level form is what removes the per-plugin burden the reviewer is asking about.

Four details that matter, learned from implementing this as a workaround:

  1. Resolve before the request struct is populated and before the keepalive pool key is derived. The pool key is built from opts.host, so if two names resolve to different addresses they must not share a pooled connection — the same invariant fix: fall back to lua_ssl_trusted_certificate for the trust store #43 established for the CA.
  2. The Host header and the SNI must keep the original name. The binding currently defaults ssl_server_name = opts.ssl_server_name or opts.host, so substituting the host first would silently make the SNI an IP address and break certificate verification against any vhost-routed upstream. The name has to be captured before substitution.
  3. IP literals should short-circuit, so the common case costs nothing.
  4. A resolution failure should surface as a connect error, matching what a cosocket does today.

Current workaround, and what the hook would remove

APISIX pre-resolves in apisix/utils/http.lua, which the AI transport calls before connect():

function _M.resolve_upstream_host(params)
    -- IP literals short-circuit
    local ip, err = core.resolver.parse_domain(host)
    ...
    params.ssl_server_name = params.ssl_server_name or host
    -- keep the name in the Host header, with the port when it is not the scheme default
    params.host = ip
end

Moving it into a shared module answered the reviewer's immediate "duplicated across many plugins" concern, but every caller still has to remember to call it before connecting, which is a footgun rather than a fix. With the hook, that helper and its call sites can be deleted outright.

References

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions