HTTPDNS域名解析之坑多多

40 阅读5分钟

背景:当前iOS客户端请求一直采用Alamofire或者Moya比较多,时常出现系统域名解析出错,运行商不稳定情况,考虑使用三方解决域名解析问题

方案:考虑使用阿里的HTTPDNS域名解析,好了至此走上一条各种坑的道路,下面我们一一说道路的各种崎岖

方案一:使用默认的SLL证书、证书评估使用[SecPolicy]()

  • 存在host:使用SecPolicyCreateSSL(true,host as CFString)
  • 不存在host:使用SecPolicyCreateBasicX509
  • 结合前后:使用SecTrustSetPolicies(serverTrust,polices as CFTypeRef)

结果:var result: SecTrustResultType = .invalid if SecTrustEvaluate(serverTrust,&result) == errSecSuccess { return result == .unspecified || result == .proceed } else { reutrn false }

完整代码如下:

class ApiAlamofireSession: Alamofire.Session {
    static let shared: ApiAlamofireSession = {
        let configuration = URLSessionConfiguration.af.default
        configuration.httpAdditionalHeaders = Alamofire.Session.default.session.configuration.httpAdditionalHeaders 
          let delegate = ApiSessionDelegate()
        return ApiAlamofireSession(configuration: configuration, delegate: delegate, serverTrustManager: nil)
    }()
} 
class ApiSessionDelegate: Alamofire.SessionDelegate {
    override func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        var disposition: URLSession.AuthChallengeDisposition = .performDefaultHandling
        var credential: URLCredential?

        if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
            let host = task.currentRequest?.value(forHTTPHeaderField: "Host")
            if evaluate(serverTrust: challenge.protectionSpace.serverTrust, host: host) {
                disposition = .useCredential
                credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
                trustManager = true
            } else {
                disposition = .performDefaultHandling
                trustManager = false
            }
        } else {
            disposition = .performDefaultHandling
            trustManager = false
        }
        completionHandler(disposition, credential)
    }

    func evaluate(serverTrust: SecTrust?, host: String?) -> Bool {
        guard let serverTrust = serverTrust else {
            return false
        }
        var policies = [SecPolicy]()
        if let host = host {
            policies.append(SecPolicyCreateSSL(true, host as CFString))
        } else {
            policies.append(SecPolicyCreateBasicX509())
        }
        SecTrustSetPolicies(serverTrust, policies as CFTypeRef)
        var result: SecTrustResultType = .invalid
        if SecTrustEvaluate(serverTrust, &result) == errSecSuccess {
            return result == .unspecified || result == .proceed
        } else {
            return false
        }
    }
}

DNS解析域名的方法如下

    let host = target.baseURL.host
    debugLog("HttpDNS~host:\(host)")
    var endpoint = MoyaProvider.defaultEndpointMapping(for: target)
    let httpdns = HttpDnsService.sharedInstance()
    if let result = httpdns?.resolveHostSync(host,by:HttpdnsQueryIPType.both),let url = URL(string:endpoint.url),trustManager == true
    {
        var urlComponents = URLComponents(url:url, resolvingAgainstBaseURL: false)
        if result.hasIpv4Address() {
            urlComponents?.host = result.firstIpv4Address()
            debugLog("HttpDNS~Get IPv4 \(result.firstIpv4Address())")
            let resultUrl = urlComponents?.url ?? url
            //设置原始的host头
            var headers = endpoint.httpHeaderFields ?? [:]
            headers["Host"] = host
            debugLog("HttpDNS~Get for host\(String(describing: host)) from HTTPDNS Successfully! resultUrl:\(resultUrl)")
            endpoint = getIpTypeEndpoint(resultUrl: resultUrl.absoluteString, target: target, headers: headers)
            debugLog("HttpDNS~endpont:\(endpoint.url)")
            
        }else if result.hasIpv6Address() {
            urlComponents?.host = result.firstIpv6Address()
            debugLog("HttpDNS~Get IPv6 \(result.firstIpv6Address())")
            let resultUrl = urlComponents?.url ?? url
            //设置原始的host头
            var headers = endpoint.httpHeaderFields ?? [:]
            headers["Host"] = host
            debugLog("HttpDNS~Get for host\(String(describing: host)) from HTTPDNS Successfully! resultUrl:\(resultUrl)")
            
            endpoint = getIpTypeEndpoint(resultUrl: resultUrl.absoluteString, target: target, headers: headers)
            debugLog("HttpDNS~endpont:\(endpoint.url)")
            
        }else{
            endpoint = getIpTypeEndpoint(resultUrl: target.baseURL.absoluteString + target.path, target: target, headers: target.headers)
        }
      
    }else{
        endpoint = getIpTypeEndpoint(resultUrl: target.baseURL.absoluteString + target.path, target: target, headers: target.headers)
    }
    switch target {
    case .pwd_login:
        timeoutInterval = 15.0
    default:
        timeoutInterval = 15.0
    }
    return endpoint
}

方案一的缺点:报错如下


                            .serverCertificateHasBadDate,     // -1201: 证书日期错误 (过期或未生效)

                            .serverCertificateUntrusted,      // -1202: 证书不被信任 (自签名、链不完整)

                            .serverCertificateHasUnknownRoot, // -1203: 证书根不受信任

                            .serverCertificateNotYetValid,    // -1204: 证书尚未生效

                            .clientCertificateRejected,       // -1205: 客户端证书被拒绝

                            .clientCertificateRequired:       // -1206: 服务器要求客户端证书
  • SecPolicyCreateBasicX509

  • 系统期望的是 SSL policy

  • 校验策略不匹配 → TLS handshake 直接失败

  • 就直接报上面的错误了,自己验证通过 ≠ 系统 TLS 认可

方案二:使用动态ServerTrustManager可以动态地将IP地址映射到其对应的域名验证策略


/// 它的作用是在进行证书验证时,强制使用我们指定的 `validationHost` (原始域名),

/// 而忽略 Alamofire 传入的 `host` (IP 地址)。

**fileprivate** **class** DomainOverridingTrustEvaluator: ServerTrustEvaluating {

    **private** **let** validationHost: String

    **private** **let** underlyingEvaluator: ServerTrustEvaluating

  


    **init**(validationHost: String, underlyingEvaluator: ServerTrustEvaluating) {

        **self**.validationHost = validationHost

        **self**.underlyingEvaluator = underlyingEvaluator

    }

  


    **func** evaluate(_ trust: SecTrust, forHost host: String) **throws** {

        // 【核心】: 忽略传入的 `host` (IP 地址),强制使用 `validationHost` (域名)

        **try** underlyingEvaluator.evaluate(trust, forHost: **self**.validationHost)

    }

}

  


/// 【新增】: 这是一个支持 HTTPDNS 的动态 ServerTrustManager。

/// 它可以动态地将 IP 地址映射到其对应的域名验证策略。

**class** HttpDnsServerTrustManager: ServerTrustManager {

    **private** **var** ipToDomainMap: [String: String] = [:]

    **private** **let** lock = NSLock()

  


    /// 外部应在发起请求前,调用此方法来注册 IP 和域名的映射关系。

    **func** add(ip: String, forDomain domain: String) {

        lock.lock(); **defer** { lock.unlock() }

        ipToDomainMap[ip] = domain

    }

  


    /// 重写 Alamofire 的核心方法。

    **override** **func** serverTrustEvaluator(forHost host: String) **throws** -> ServerTrustEvaluating? {

        lock.lock(); **defer** { lock.unlock() }

  


        // `host` 在我们的场景中是 IP 地址。

        // 1. 尝试从动态映射中找到这个 IP 对应的原始域名。

        **if** **let** domain = ipToDomainMap[host] {

            // 2. 如果找到了域名,就从父类的 evaluators 字典中查找为【该域名】配置的评估器。

            **if** **let** domainEvaluator = evaluators[domain] {

                // 3. 返回一个包装后的评估器,确保验证时使用域名而不是 IP。

                **return** DomainOverridingTrustEvaluator(validationHost: domain, underlyingEvaluator: domainEvaluator)

            }

        }

        

        // 4. 如果找不到映射,则回退到默认行为。

        **return** **try** **super**.serverTrustEvaluator(forHost: host)

    }

}

使用动态域名解析可以解决大部分问题,但是还是有部分用户报错

ChatGPT Image 2026年1月15日 12_24_53.png 意外不意外,惊喜不惊喜😒

  • 排查结果一:SSL/TLS 证书或安全协议错误::errorCode: 6, errorUserInfo: ["NSLocalizedDescription": "Server trust evaluation failed due to reason: SecTrust evaluation failed with error: “*.123.com”证书不符合标准", "NSUnderlyingError": Alamofire.AFError.serverTrustEvaluationFailed(reason: Alamofire.AFError.ServerTrustFailureReason.trustEvaluationFailed(error: Optional(Error Domain=NSOSStatusErrorDomain Code=-67825 "“*.123.com”证书不符合标准" UserInfo={NSLocalizedDescription=“*.123.com”证书不符合标准, NSUnderlyingError=0x1097cd800 {Error Domain=NSOSStatusErrorDomain Code=-67825 "证书0 “*.123.com”有错误:本次使用需要证书透明度验证;" UserInfo={NSLocalizedDescription=证书0 “*.123.com”有错误:本次使用需要证书透明度验证;奇怪吧又戳戳来一堆错误
  • 好的不算差,最起码有错误日志不是,显示证书没有透明度验证?
  • 从 iOS 12.1.1+ 开始,Apple 强制要求所有 TLS 证书必须包含 SCT(Signed Certificate Timestamp) 记录,这是证书透明度的一部分。您的服务器证书可能:
    1. ❌ 没有包含 SCT 日志
    1. ❌ SCT 日志不完整或过期
    1. ❌ 使用了旧的 CA 签发的证书

好吧兄弟们把锅甩出去吧,找运维去哈

十分钟后。。。。。。。。

  • ✅ 证书本身没问题
  • ✅ 是否含有CT政策: 符合
  • ✅ 颁发者:RapidSSL TLS RSA CA G1(DigiCert)
  • ✅ 信任状态:可信
  • ✅ 有效期:正常(剩余 375 天)

证书本身符合 CT 要求,问题在于其他地方,好吧继续。。。。。

因为我们用的是HTTPDNS将域名替换为IP,会导致urlComponents.host = ip看着也没错呀,HTTPDNS的作用不就是这个吗????? 好的,那就查查这个流程吧,到底干了啥?

  • HTTPDNS 将 123.com 解析为 IP(如 1.2.3.4)
  • 请求 URL 变成 https://1.2.3.4/api/xxx
  • 服务器返回 *.123.com 证书
  • iOS 验证证书时,发现 URL host 是 IP,但证书是域名
  • CT 验证失败(因为 SNI 或 host 不匹配)

真相大白了,我们的流程就是这样的呀 所以再加个容错吧,万分之一的概率出错 解决方法如下

  1. HTTPDNS 解析:123.com → 1.2.3.4 ↓
  2. URL 变成:https://1.2.3.4/api/xxx
  3. trustManager.add(ip: "1.2.3.4", forDomain: "123.com") ↓
  4. 发起请求,Host header = 123.com ↓
  5. ServerTrustManager 验证证书:
    • host = "1.2.3.4" (URL 的 host)
    • 查找 ipToDomainMap["1.2.3.4"] → "123.com"
    • 查找 evaluators["123.com"] → DefaultTrustEvaluator()
    • 使用域名 "123.com" 验证证书 ✅

至此全部解决兄弟们,加一次域名请求的兜底,保证失效状态的重试