iOS 26 Network Framework AWDL not working

Hello,

I have an app that is using iOS 26 Network Framework APIs.

It is using QUIC, TLS 1.3 and Bonjour. For TLS I am using a PKCS#12 identity.

All works well and as expected if the devices (iPhone with no cellular, iPhone with cellular, and iPad no cellular) are all on the same wifi network.

If I turn off my router (ie no more wifi network) and leave on the wifi toggle on the iOS devices - only the non cellular iPhone and iPad are able to discovery and connect to each other. My iPhone with cellular is not able to.

By sharing my logs with Cursor AI it was determined that the connection between the two problematic peers (iPad with no cellular and iPhone with cellular) never even makes it to the TLS step because I never see the logs where I print out the certs I compare.

I tried doing "builder.requiredInterfaceType(.wifi)" but doing that blocked the two non cellular devices from working. I also tried "builder.prohibitedInterfaceTypes([.cellular])" but that also did not work.

Is AWDL on it's way out? Should I focus my energy on Wi-Fi Aware?

Regards, Captadoh

Answered by DTS Engineer in 867952022

Did you opt in to peer-to-peer on both the listener and the browser? We never enable that by default because it has a non-trivial network impact.

IIRC, this is how you’d do that on the listener:

let lp = BonjourListenerProvider(type: "_test._tcp")
let l = try NetworkListener(
    for: lp,
    using: .parameters({
        TCP()
    })
    .peerToPeerIncluded(true)
)

And a similar technique is available for the browser.

Share and Enjoy

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

Did you opt in to peer-to-peer on both the listener and the browser? We never enable that by default because it has a non-trivial network impact.

IIRC, this is how you’d do that on the listener:

let lp = BonjourListenerProvider(type: "_test._tcp")
let l = try NetworkListener(
    for: lp,
    using: .parameters({
        TCP()
    })
    .peerToPeerIncluded(true)
)

And a similar technique is available for the browser.

Share and Enjoy

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

Did you opt in to peer-to-peer on both the listener and the browser?

I did do that. Please see my code snippets below.

    private func startBrowser() async throws {
        let parameters = NWParameters()
        parameters.includePeerToPeer = true
        // parameters.requiredInterfaceType = .wifi
       // parameters.prohibitedInterfaceTypes = [.cellular]

        try await NetworkBrowser(
            for: .bonjour(serviceName, domain: nil, includeTxtRecord: false),
            using: parameters
        )
        .onStateUpdate { _, state in
            switch state {
            case let .failed(error):
                logger.error("[startBrowser]:browser failed: -> \(error)")
            case .ready:
                logger.info("[startBrowser]: browser is ready")
            case .cancelled:
                logger.error("[startBrowser]: browser cancelled")
            case .setup:
                logger.info("[startBrowser]: browser in setup")
            case let .waiting(error):
                logger.error("[startBrowser]: browser is waiting: \(error)")
            @unknown default:
                break
            }
        }
        .run { endpoints in
            self.handleBrowserEndpoints(endpoints)
        }
    }
    private func startListener() async throws {
        guard let identity = Utils.importIdentityFromPKCS12() else {
            logger.error("could not get identity")
            return
        }
        guard let certificateData = Utils.getCertificateData(from: identity) else {
            logger.error("could not read certificate data")
            return
        }
        let builder = try makeQUICParametersBuilder(identity: identity, certificateData: certificateData)

        @SharedReader(.thisDeviceID) var thisDeviceID

        let listener = try NetworkListener(for: .bonjour(name: nil, type: serviceName), using: builder)
        listener.service = NWListener.Service(name: thisDeviceID, type: serviceName)

        try await listener.onServiceRegistrationUpdate { _, change in
            switch change {
            case let .add(endpoint):
                logger.info("onServiceRegistrationUpdate: added \(endpoint.debugDescription)")
            case let .remove(endpoint):
                logger.info("onServiceRegistrationUpdate: removed \(endpoint.debugDescription)")
            @unknown default:
                return
            }
        }
        .onStateUpdate { lst, state in
            logger.info("\(String(describing: lst)): \(String(describing: state))")
            switch state {
            case let .failed(error):
                logger.error("Listener failed: \(error)")
            case .ready:
                logger.info("Listener ready")
            case .cancelled:
                logger.info("listener cancelled")
            case let .waiting(error):
                logger.error("listener is waiting: \(error)")
            case .setup:
                logger.info("listener setup")
            @unknown default:
                break
            }
        }
        .run { connection in
            logger.info("listener run callback received connection")
            await self.handleInboundConnection(connection)
        }
    }
    private func makeQUICParametersBuilder(identity: SecIdentity, certificateData: Data) throws -> NWParametersBuilder<QUIC> {
        guard let nwIdentity = sec_identity_create(identity) else {
            throw NetworkingError.failedToCreateIdentity
        }

        var quic = QUIC(alpn: ["captadoh"])
        quic = quic.idleTimeout(0)
        quic = quic.tls.localIdentity(nwIdentity)
        quic = quic.tls.certificateValidator { _, secTrust async -> Bool in
            let trust = sec_trust_copy_ref(secTrust).takeRetainedValue()

            guard let certificates = SecTrustCopyCertificateChain(trust) as? [SecCertificate],
                  let peerCertificate = certificates.first
            else {
                logger.error("Failed to get peer certificate")
                return false
            }

            let peerCertificateData = SecCertificateCopyData(peerCertificate) as Data
            logger.info("Peer cert SHA256: \(SHA256.hash(data: peerCertificateData).map { String(format: "%02x", $0) }.joined())")
            logger.info("Local cert SHA256: \(SHA256.hash(data: certificateData).map { String(format: "%02x", $0) }.joined())")
            return peerCertificateData == certificateData
        }
        quic = quic.tls.peerAuthentication(.required)

        var builder = NWParametersBuilder<QUIC>.parameters {
            quic
        }
        builder = builder.peerToPeerIncluded(true)
        // builder = builder.requiredInterfaceType(.wifi)
        // builder = builder.prohibitedInterfaceTypes([.cellular])
        return builder
    }

To add more info to this I tried the following.

I have this going in my app:

        let m = NWPathMonitor()
        m.pathUpdateHandler = { [weak self] path in
            print("pathUpdateHandler: \(path)")
            print("path.debugDescription: \(path.debugDescription)")
            print("path.unsatisfiedReason: \(path.unsatisfiedReason)")
            print("path.status: \(path.status)")
            print("path.isExpensive: \(path.isExpensive)")
            print("path.isConstrained: \(path.isConstrained)")
            print("path.isUltraConstrained: \(path.isUltraConstrained)")
            print("path.availableInterfaces: \(path.availableInterfaces)")
            print("path.availableInterfaces.count: \(path.availableInterfaces.count)")
            path.availableInterfaces.forEach { print("interface: \($0)") }
            Task { await self?.broadcast(path) }
        }

On the iPhone with NO cellular (device A) - while on the wifi network - I see [en0, en0] for availableInterfaces

On the iPhone with cellular (device B) - while on the wifi network - I see [en0, en0, pdp_ip0, pdp_ip0] for availableInterfaces

When i cut off the wifi network device A shows [] for availableInterfaces

When i cut off the wifi network device B shows [pdp_ip0, pdp_ip0] for availableInterfaces

When i then toggle on airplane mode on device B but toggle on wifi I see [] for availableInterfaces

So at this point device A and device B are showing the same availableInterfaces - but they still can't connect. They can discovery each other but the connection never makes it to the TLS stage.

Below are my logs from having no wifi network, device A has its Device Discovery running (ie NetworkBrowser up and running and NetworkListener up and running) - device A has its wifi toggle on

device B has its Device Discovery running (ie NetworkBrowser up and running and NetworkListener up and running) - device B has its wifi toggle on

[MessageService] new subscriber added
local network service -> browser state update handler: ready
unsubscribe voice control commands
here in on termination of voice control
local network service -> browser state update handler: cancelled
[startBrowser]: browser is ready
[L1 ready, local endpoint: <NULL>, parameters: quic, local: ::.0, definite, attribution: developer, server, port: 61566, path satisfied (Path is satisfied), viable, interface: pdp_ip0[lte], ipv4, ipv6, dns, expensive, uses cell, LQM: good, service: 8FB0D9E1-303A-47E2-B15A-6826279577D7._captadoh._udp.<NULL> txtLength:0]: ready
Listener ready
onServiceRegistrationUpdate: added 8FB0D9E1-303A-47E2-B15A-6826279577D7._captadoh._udp.local.
[NetworkingService] handleBrowserEndpoints skipping self endpoint: 8FB0D9E1-303A-47E2-B15A-6826279577D7
handleOutboundConnection: connection added
here in setupConnectionReceiveTask
nw_resolver_start_query_timer_block_invoke [C2.1.1] Query fired: did not receive all answers in time for b4f40bc0-c586-4946-a2f3-950fb21fddeb.local.:64458
[NetworkingService] handleBrowserEndpoints skipping self endpoint: 8FB0D9E1-303A-47E2-B15A-6826279577D7
[NetworkingService] handleEndpointRemoved removed endpoint for 354C4D39-2CA4-4A06-9F41-8A9B755C025B
nw_connection_copy_protocol_metadata_internal_block_invoke [C3] Client called nw_connection_copy_protocol_metadata_internal on unconnected nw_connection
nw_connection_copy_protocol_metadata_internal_block_invoke [C3] Client called nw_connection_copy_protocol_metadata_internal on unconnected nw_connection
nw_connection_copy_connected_local_endpoint_block_invoke [C3] Client called nw_connection_copy_connected_local_endpoint on unconnected nw_connection
nw_connection_copy_connected_remote_endpoint_block_invoke [C3] Client called nw_connection_copy_connected_remote_endpoint on unconnected nw_connection
nw_endpoint_flow_failed_with_error [C2.1.1.1 fe80::7cc9:daff:fea6:9151%awdl0.64458 in_progress channel-flow (satisfied (Path is satisfied), viable, interface: awdl0[802.11], scoped, uses wifi, LQM: good)] already failing, returning
[NetworkingService] handleBrowserEndpoints skipping self endpoint: 8FB0D9E1-303A-47E2-B15A-6826279577D7
handleOutboundConnection: connection added
here in setupConnectionReceiveTask
nw_resolver_start_query_timer_block_invoke [C4.1.1] Query fired: did not receive all answers in time for b4f40bc0-c586-4946-a2f3-950fb21fddeb.local.:64458
[NetworkingService] handleBrowserEndpoints skipping self endpoint: 8FB0D9E1-303A-47E2-B15A-6826279577D7
[NetworkingService] handleEndpointRemoved removed endpoint for 354C4D39-2CA4-4A06-9F41-8A9B755C025B
nw_connection_copy_protocol_metadata_internal_block_invoke [C5] Client called nw_connection_copy_protocol_metadata_internal on unconnected nw_connection
nw_connection_copy_protocol_metadata_internal_block_invoke [C5] Client called nw_connection_copy_protocol_metadata_internal on unconnected nw_connection
nw_connection_copy_connected_local_endpoint_block_invoke [C5] Client called nw_connection_copy_connected_local_endpoint on unconnected nw_connection
nw_connection_copy_connected_remote_endpoint_block_invoke [C5] Client called nw_connection_copy_connected_remote_endpoint on unconnected nw_connection
nw_endpoint_flow_failed_with_error [C4.1.1.1 fe80::7cc9:daff:fea6:9151%awdl0.64458 in_progress channel-flow (satisfied (Path is satisfied), viable, interface: awdl0[802.11], scoped, uses wifi, LQM: good)] already failing, returning

Note that device A has the device ID 354C4D39-2CA4-4A06-9F41-8A9B755C025B - device B has the device ID 8FB0D9E1-303A-47E2-B15A-6826279577D7

iOS 26 Network Framework AWDL not working
 
 
Q