Limits: Custom DNS resolver vs Host header mismatch in Ktor client
0 reputation · 17 Sept 2023, 16:39 UTC
0 reputation · 17 Sept 2023, 16:39 UTC
We want to issue an HTTPS request to a server identified by an IP address while setting the Host header to a domain that has a valid TLS certificate. The request should perform DNS resolution via a custom resolver so that the domain maps to the desired IP, and the TLS handshake should validate the certificate against that domain.
Using the default Jetty engine, Ktor delegates DNS lookup to the JVM’s java.net.InetAddress resolver. The engine does not expose a public API to swap the resolver without changing the underlying engine. Setting validateCertificates = false disables hostname checks entirely, which is unacceptable for production use.
When a custom resolve function is supplied in the Host header, the underlying TLS handshake still uses the IP address resolved by the JVM, leading to a hostname mismatch and certificate validation failure. It is unclear whether Ktor can be configured to use the custom resolver for both the HTTP request and the TLS handshake, or if this behavior is an inherent limitation.
validateCertificates?31775 reputation · 18 Sept 2023, 01:42 UTC
No — there is no documented Ktor-level API to inject a custom DNS resolver into the Jetty engine, and a Host header override can never fix this. The clean solution is to keep the domain in the request URL and switch to the OkHttp engine, whose Dns interface is a documented extension point that maps the domain to your IP while TLS SNI and hostname verification continue to use the domain.
TLS hostname verification happens during the handshake, against the hostname used to open the connection — i.e., the host in the request URL. The Host header is application-layer data sent after the handshake completes, so it plays no role in certificate validation. If your URL contains a raw IP, the certificate must contain that IP as a SAN entry; otherwise verification fails no matter what header you set. This is confirmed TLS behavior, not a Ktor quirk.
The likely explanation for what you observed: with the Jetty engine, Ktor delegates resolution to the JVM's InetAddress resolver, and there is no public hook to replace it per-client. You would have to reach into engine-internal socket factory wiring, which is version-sensitive across Ktor 2.x/3.x and fragile.
OkHttp separates which hostname is verified (always the URL host) from which address is dialed (the Dns lookup). That is exactly your requirement:
val client = HttpClient(OkHttp) {
engine {
config {
dns(object : Dns {
override fun lookup(hostname: String): List<InetAddress> =
if (hostname == "api.example.com")
listOf(InetAddress.getByName("203.0.113.10"))
else Dns.SYSTEM.lookup(hostname)
})
}
}
}
// Request the domain, not the IP:
client.get("https://api.example.com/health")SNI carries api.example.com, the certificate is verified against that name, and the connection goes to your chosen IP. Certificate validation stays fully enabled.
validateCertificates = false, since that masks the exact failure mode you are testing.127.0.0.1 in your Dns override and assert the request succeeds. Then re-run with a certificate missing the domain SAN: it must fail. That negative test proves hostname checks are still active.-Djavax.net.debug=ssl:handshake or capturing the handshake, and log the address your resolver returns versus the address actually dialed.Pinning a production domain to a fixed IP bypasses DNS-based failover and load balancing — treat it as an operational decision with a maintenance cost. Also verify the exact Dns wiring against your OkHttp/Ktor version; engine config lambdas differ between Ktor 2.x and 3.x. If you are pinned to the Jetty engine, the fallback is a custom SSLSocketFactory that dials the fixed IP while SSL parameters retain the domain, but that is more code and more risk than switching engines.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.