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

@DTS Engineer - Here is how I build an outbound connection:

    private func handleOutboundConnection(to endpoint: NWEndpoint, deviceID: DeviceID) {
        do {
            guard let identity = Utils.importIdentityFromPKCS12() else {
                logger.error("[networkBrowser] could not import identity")
                return
            }
            guard let certificateData = Utils.getCertificateData(from: identity) else {
                logger.error("[networkBrowser] could not read certificate data")
                return
            }

            let builder = try makeQUICParametersBuilder(identity: identity, certificateData: certificateData)
            let connection = NetworkConnection(to: endpoint, using: builder)
            let connectionID = ConnectionID(connection)

//            connectionIDToConnection[connectionID] = connection
//            associateConnection(connectionID, with: deviceID)

            print("handleOutboundConnection: connection added")

            setupConnectionStateTask(for: connection, connectionID: connectionID, origin: .browser, deviceID: deviceID)
            setupConnectionReceiveTask(for: connection, connectionID: connectionID)
        } catch {
            print("could not create NetworkConnection: \(error)")
        }
    }

@DTS Engineer bumping to not lose eyes

Bump to not lose visibility.

I also tried loading my app onto two physical iPhones (both running iOS 26) that have cellular and they were not able to discover each other.

I also tried

builder = builder.expensivePathsProhibited(true)

and

parameters.prohibitExpensivePaths = true

but it did not help.

Hi @DTS Engineer - I wanted to check in and see if there is any things else I can shared that would help in troubleshooting my issue?

Bumping to not lose visibility.

Hi Quinn @DTS Engineer I am curious if my post is missing any information needed to troubleshoot my issue.

I searched through the forums and online and haven't found my specific issue anywhere.

The more I think about it I come to the following:

  • true peer to peer with no wifi network works with iOS devices without cellular (so AWDL is used)
  • once cellular is introduced there needs to be a wifi network because using AWDL is not an option

Maybe some kind of option can be passed into the NetworkListener or when creating a NetworkConnection that would prompt apple's code to try to use AWDL if the peer to peer option is true?

Share and Enjoy

Sorry I didn’t reply earlier; I was hoping to do an full test of this but have been struggling to find the time to set that up.

It’s best not to think it terms of AWDL. It’s an implementation detail of Apple’s peer-to-peer Wi-Fi feature. That implementation has changed in the past and it’s not hard to imagine it changing again in the future. So, while the existence of AWDL is not a secret, and it’s certainly useful to know about it when looking and logs and so on, the feature we’re talking about is Apple peer-to-peer Wi-Fi.

true peer to peer with no wifi network works with iOS devices

Correct.

without cellular

I’m not sure why you think cellular is a factor here. It is not. Peer-to-peer Wi-Fi is independent of cellular.

once cellular is introduced there needs to be a wifi network because using AWDL is not an option

No, you’re really off in the weeds here. Peer-to-peer Wi-Fi and cellular are completely independent subsystems. You can confirm this at the user level using AirDrop:

  1. Using two iPhones…
  2. Disable cellular.
  3. And forget all Wi-Fi networks.
  4. AirDrop between them works, and it works using peer-to-peer Wi-Fi.

One thing I was able to test recently is peer-to-peer Wi-Fi using TCP with the old Network framework API (NWListener and NWConnection). I was hoping to extend that test to first use the new API (NetworkListener and NetworkConnection) and then switch to QUIC. Unfortunately I haven’t had a chance to do that. But it does suggest a path forward for you, namely:

  1. Create a small test app that uses the old API and TCP. Confirm that this works.
  2. Then convert it to using the new API. Does it still work?
  3. If so, switch it over to QUIC. Does it still work?

If this all works, you’ve solved your problem. If not, I’d be interested in hearing about how far you got and how it failed.

Share and Enjoy

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

  • Create a small test app that uses

Are you open to me inviting you to a private Github repo?

I’m not sure why you think cellular is a factor here. It is not. Peer-to-peer Wi-Fi is independent of cellular.

I think this because things work (ie multiple devices are able to discover and connect to each other when no wifi network is available) only for devices without cellular. If i load the exact same app onto devices with cellular and try the same thing they are not able to discovery and connect to each other.

  • Create a small test app

I created four branches on my app:

  • ios18_network_api_tcp
  • ios18_network_api_quic
  • ios26_network_api_tcp
  • ios26_network_api_quic

I have three physical devices to test with:

  • iPad (Device A - iOS 26 - no cellular)
  • iPhone (Device B - iOS 26 - no cellular)
  • iPhone (Device C - iOS 26 - yes cellular)

For each branch listed above I tested two scenarios:

  • home Wi-Fi network ON
  • home Wi-Fi network OFF (I physically turned off the router)

Note that on each device the Wi-Fi toggle is ON.

Results from each branch for home Wi-Fi ON:

  • Devices A, B and C can all discovery and connect to each other

Results from each branch for home Wi-Fi OFF:

  • Devices A and B can discovery and connect to each other
  • Device C is not able to discovery or connect to the other devices

Well, that’s weird. And it doesn’t gel with my experience. Admittedly, that was all prior to iOS 26, so it’s possible we broke something.

I’m gonna try to reproduce this here in my office, but I won’t have time to do that until after the winter break.

Are you open to me inviting you to a private Github repo?

It’s much more convenient to do this with public source code. Keep in mind that you don’t need a full app to test this. Just getting TCP to connect (or the QUIC tunnel) is sufficient to confirm that peer-to-peer Wi-Fi is working.

Share and Enjoy

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

iOS 26 Network Framework AWDL not working
 
 
Q