We are facing a DNS resolution issue with a specific ISP, where our domain name does not resolve correctly using the system DNS. However, the same domain works as expected when a custom DNS resolver is used.
On Android, this is straightforward to handle by configuring a custom DNS implementation using OkHttp / Retrofit. I am trying to implement a functionally equivalent solution in native iOS (Swift / SwiftUI).
Android Reference (Working Behavior) :
val dns = DnsOverHttps.Builder() .client(OkHttpClient()) .url("https://cloudflare-dns.com/dns-query%22.toHttpUrl()) .bootstrapDnsHosts(InetAddress.getByName("1.1.1.1")) .build() OkHttpClient.Builder() .dns(dns) .build()
Attempted iOS Approach
I attempted the following approach :
- Resolve the domain to an IP address programmatically (using DNS over HTTPS)
- Connect directly to the resolved IP address
- Set the original domain in the Host HTTP header
DNS Resolution via DoH :
func resolveDomain(domain: String) async throws -> String { guard let url = URL( string: "https://cloudflare-dns.com/dns-query?name=(domain)&type=A" ) else { throw URLError(.badURL) }
var request = URLRequest(url: url) request.setValue("application/dns-json", forHTTPHeaderField: "accept")
let (data, _) = try await URLSession.shared.data(for: request) let response = try JSONDecoder().decode(DNSResponse.self, from: data)
guard let ip = response.Answer?.first?.data else { throw URLError(.cannotFindHost) }
return ip }
API Call Using Resolved IP :
func callAPIUsingCustomDNS() async throws { let ip = try await resolveDomain(domain: "example.com")
guard let url = URL(string: "https://(ip)") else { throw URLError(.badURL) }
let configuration = URLSessionConfiguration.ephemeral let session = URLSession( configuration: configuration, delegate: CustomURLSessionDelegate(originalHost: "example.com"), delegateQueue: .main )
var request = URLRequest(url: url) request.setValue("example.com", forHTTPHeaderField: "Host")
let (_, response) = try await session.data(for: request) print("Success: (response)") }
Problem Encountered
When connecting via the IP address, the TLS handshake fails with the following error: Error Domain=NSURLErrorDomain Code=-1200 "A TLS error caused the secure connection to fail."
This appears to happen because iOS sends the IP address as the Server Name Indication (SNI) during the TLS handshake, while the server’s certificate is issued for the domain name.
Custom URLSessionDelegate Attempt :
class CustomURLSessionDelegate: NSObject, URLSessionDelegate {
let originalHost: String
init(originalHost: String) { self.originalHost = originalHost }
func urlSession( _ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void ) { guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, let serverTrust = challenge.protectionSpace.serverTrust else { completionHandler(.performDefaultHandling, nil) return }
let sslPolicy = SecPolicyCreateSSL(true, originalHost as CFString) let basicPolicy = SecPolicyCreateBasicX509() SecTrustSetPolicies(serverTrust, [sslPolicy, basicPolicy] as CFArray)
var error: CFError? if SecTrustEvaluateWithError(serverTrust, &error) { completionHandler(.useCredential, URLCredential(trust: serverTrust)) } else { completionHandler(.cancelAuthenticationChallenge, nil) } } }
However, TLS validation still fails because the SNI remains the IP address, not the domain.
I would appreciate guidance on the supported and App Store–compliant way to handle ISP-specific DNS resolution issues on iOS. If custom DNS or SNI configuration is not supported, what alternative architectural approaches are recommended by Apple?
I attempted the following approach
This approach won’t work in general. iOS devices regularly find themselves in situations where the traditional resolve-then-connect approach won’t work. I talk about this in general terms in the Connect by name section of TN3151 Choosing the right networking API
Additionally, this is problematic:
Set the original domain in the Host HTTP header
The Host header is ‘owned’ by URLSession and not something you can reliably set. See here.
Fortunately, there’s an alternative approach that should work, namely to set a per-app DNS resolver. You can do this via Network framework’s privacy context mechanism. See this post for an example.
ps It’s really hard to read your post. To avoid problems like that in the future, have a read of Quinn’s Top Ten DevForums Tips, and specifically the discussion of code blocks in tip 5.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"