NWConnection WebSocket Protocol hangs on preparing for iOS 13 only

I have a critical issue where my websocket will not connect to a server that is sitting behind an NGINX reverse proxy only on iOS 13. I have tested on both a real device and simulator with no success. It simply hangs on preparing. it never receives updates to the viabilityUpdateHandler and only ever enters the preparing state of the stateUpdateHandler. On any iOS greater or equal to iOS 14 it works seamlessly. I can connect to a local server that is not dealing with any certificates on iOS 13 no problem, but when my production server is in play it does not communicate something properly.

I am using NWConnection's NWProtocolWebSocket.

The setup is basic and straight forward

        let options = NWProtocolWebSocket.Options()
        options.autoReplyPing = configuration.autoReplyPing
        options.maximumMessageSize = configuration.maximumMessageSize
        
        if configuration.urlRequest != nil {
            options.setAdditionalHeaders(configuration.urlRequest?.allHTTPHeaderFields?.map { ($0.key, $0.value) } ?? [])
            _ = configuration.cookies.map { cookie in
                options.setAdditionalHeaders([(name: cookie.name, value: cookie.value)])
            }
        }
        if !configuration.headers.isEmpty {
            options.setAdditionalHeaders(configuration.headers.map { ($0.key, $0.value) } )
        }
        
        let parameters: NWParameters = configuration.trustAll ? try TLSConfiguration.trustSelfSigned(
            configuration.trustAll,
            queue: configuration.queue,
            certificates: configuration.certificates) : (configuration.url.scheme == "ws" ? .tcp : .tls)
        parameters.defaultProtocolStack.applicationProtocols.insert(options, at: 0)
        connection = NWConnection(to: .url(configuration.url), using: parameters)

The trust store is also straight forward

    public static func trustSelfSigned(_
                                trustAll: Bool,
                                queue: DispatchQueue,
                                certificates: [String]?
    ) throws -> NWParameters {
        let options = NWProtocolTLS.Options()
        
        var secTrustRoots: [SecCertificate]?
        secTrustRoots = try certificates?.compactMap({ certificate in
            let filePath = Bundle.main.path(forResource: certificate, ofType: "der")!
            let data = try Data(contentsOf: URL(fileURLWithPath: filePath))
            return SecCertificateCreateWithData(nil, data as CFData)!
        })
        
        sec_protocol_options_set_verify_block(
            options.securityProtocolOptions,
            { _, sec_trust, sec_protocol_verify_complete in
                guard !trustAll else {
                    sec_protocol_verify_complete(true)
                    return
                }
                let trust = sec_trust_copy_ref(sec_trust).takeRetainedValue()
                if let trustRootCertificates = secTrustRoots {
                    SecTrustSetAnchorCertificates(trust, trustRootCertificates as CFArray)
                }
                dispatchPrecondition(condition: .onQueue(queue))
              
                    SecTrustEvaluateAsyncWithError(trust, queue) { _, result, error in
                        if let error = error {
                            print("Trust failed: \(error.localizedDescription)")
                        }
                        print("Validation Result: \(result)")
                        sec_protocol_verify_complete(result)
                    }
                },
            queue
        )
     sec_protocol_options_set_min_tls_protocol_version(options.securityProtocolOptions, .TLSv12)
        
        let parameters = NWParameters(tls: options)
        
        parameters.allowLocalEndpointReuse = true
        parameters.includePeerToPeer = true
        return parameters
    }

Is your verify block — the one being passed to sec_protocol_options_set_verify_block — being called at all?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

No the verify block is not called from the method sec_protocol_options_set_verify_block, but only on iOS 13. Not sure why this would happen. I am using the same queue as the one NWConnection is using. What would cause the block not to be called?

For those following at at home, I’ll be helping Cartisim in a different context.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

NWConnection WebSocket Protocol hangs on preparing for iOS 13 only
 
 
Q