UDP Broadcast on iOS18

I am writing an app using Microsoft's MAUI platform. I am posting this here because that team wants me to make an xcode project to help determine an issue I am having.

My MAUI app sends a broadcast packet on a UDP socket using address 255.255.255.255. This worked fine in iOS version 17.x. After upgrading my phone to iOS 18.x it stopped working.

The error I get is "no route to host".

The exact same code works fine on MacOS. It does not work on iPadOs 18.

My question here is 3 fold:

  1. Did something specific change between iOS 17 and 18 that would cause a 'no route to host' error when sending a UDP broadcast packet?

  2. Can someone provide sample code to show me how to do this type of broadcast using Swift in Xcode for iOS?

  3. I read an article that said my app would need the com.apple.developer.networking.multicast entitlement in order to use boradcast functionality. This was introduced in iOS 14. Why did my app work fine in iOS 17 then? Is this what changed? Did this requirement use to be optional and is now required? I did get this entitlement from Apple and applied it to my provisioning profile and my app gave the same "no route to host" error. Why?

I found some Swift code, but when I try to use it I get a different error. In the following code the status goes from preparing to waiting. I have show both the code and the logs in Xcode below.

Thoughts?

//
//  ContentView.swift
//  UDP Broadcast Test
//
//  Created by Tony Pitman on 12/19/24.
//

import SwiftUI
import Network

struct ContentView: View {
    
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, world!")
            Button("UDP Test") {
                
                let connection = NWConnection(host: "255.255.255.255", port: 11000, using: .udp)
                connection.stateUpdateHandler = { (newState) in
                    print("This is stateUpdateHandler:")
                    switch (newState) {
                    case .ready:
                        print("State: Ready\n")
                        self.sendUDP(connection: connection, content: "Polaris Discovery")
                        self.receiveUDP(connection: connection)
                    case .setup:
                        print("State: Setup\n")
                    case .cancelled:
                        print("State: Cancelled\n")
                    case .preparing:
                        print("State: Preparing\n")
                    case .waiting(let error):
                        print("State: Waiting: \(error)\n")
                    default:
                        print("ERROR! State not defined!\n")
                    }
                }
                
                connection.start(queue: .global())
            }
        }
        .padding()
    }
    
    func sendUDP(connection: NWConnection, content: Data) {
        connection.send(content: content, completion: NWConnection.SendCompletion.contentProcessed(({ (NWError) in
            if (NWError == nil) {
                print("Data was sent to UDP")
            } else {
                print("ERROR! Error when data (Type: Data) sending. NWError: \n \(NWError!)")
            }
        })))
    }
    
    func sendUDP(connection: NWConnection, content: String) {
        let contentToSendUDP = content.data(using: String.Encoding.utf8)
        connection.send(content: contentToSendUDP, completion: NWConnection.SendCompletion.contentProcessed(({ (NWError) in
            if (NWError == nil) {
                print("Data was sent to UDP")
            } else {
                print("ERROR! Error when data (Type: Data) sending. NWError: \n \(NWError!)")
            }
        })))
    }
    
    func receiveUDP(connection: NWConnection) {
        connection.receiveMessage { (data, context, isComplete, error) in
            if (isComplete) {
                print("Receive is complete")
                if (data != nil) {
                    let backToString = String(decoding: data!, as: UTF8.self)
                    print("Received message: \(backToString)")
                } else {
                    print("Data == nil")
                }
            }
        }
    }
}

#Preview {
    ContentView()
}

Log output including the waiting error:

NSBundle file:///System/Library/PrivateFrameworks/MetalTools.framework/ principal class is nil because all fallbacks have failed
This is stateUpdateHandler:
This is stateUpdateHandler:
State: Preparing

This is stateUpdateHandler:
State: Waiting: POSIXErrorCode(rawValue: 50): Network is down

State: Waiting: POSIXErrorCode(rawValue: 50): Network is down

Much as I’m a huge fan of Network framework, it’s not currently capable of dealing with UDP broadcasts. I tried to make that clear in TN3151 Choosing the right networking API. And I recently added a new post, Broadcasts and Multicasts, Hints and Tips, to my Extra-ordinary Networking collection which goes into much greater detail. Please read that thread and then reply here if you have follow-up questions.

Share and Enjoy

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

I do have one question:

Why did this work last summer? At least it seemed to.

I had a working app last summer and then didn't work on it for a few months. I then had upgraded my phone's OS and xcode to 16.2 (before I was on 15.x).

It quit working around October / November.

I thought maybe it was the OS upgrade, but it also didn't work on 17 at that time.

Did something change in Xcode 16 or in the iOS SDK maybe?

Why did this work last summer?

I’m not sure. The issue here is that, when you do this wrongly, your code ends up relying on undefined behaviour. That behaviour can change for all sorts of reasons, so it’s hard to say exactly why something failed or worked.

So, rather than focus on why it stopped working, my general advice is that you focus on getting it working, and getting it working in a way that won’t break in the future.

Which brings me back to the big picture. What are these broadcasts meant to do? That’ll inform how you decide which set of interfaces to broadcast on. For example, if the user connects an Ethernet interface to their iOS device, do you want the broadcasts to be sent over Ethernet, over Wi-Fi, or both?

Share and Enjoy

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

We have a hardware device based on the esp32.

This device will connect to the user’s wi-fi and then start watching for UDP broadcasts.

Our mobile app sends a UDP broadcast.

Our device then replies to the broadcast with a specially formatted packet containing its IP address.

Our mobile app can then make a TCP/IP connection and go from there.

Ah, service discovery. I specifically talk about this in Extra-ordinary Networking > Don’t Try to Get the Device’s IP Address > Service Discovery. You might also find Extra-ordinary Networking > Working with a Wi-Fi Accessory generally useful.

You might be tempted to send these broadcasts only over Wi-Fi, because your accessory only supports Wi-Fi. I encourage you to think carefully about that choice. If your accessory is deployed to a wide audience, you probably want to send your broadcasts over all broadcast-capable interfaces. I regularly bump into folks with a setup like this:

      iPhone                                     Internet
         |                                          |
USB Ethernet adapter Ethernet                     router
         |                                          |
---------+------+-----------------------------------+---- Ethernet
                |
       AP with fancy firewall
                |
----------------+-----------+---------------------------- Wi-Fi
                            |
                        accessory

This seems bananas, but there’s a good reason: Their org does not want secure-sensitive devices on Wi-Fi.

Oh, and the other case where Ethernet matters is if you allow the user to run your iOS app on the Mac.

Share and Enjoy

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

I have no reason to limit it to Wi-Fi.

I have not finished reading your article yet.

Does it contain or point to example code showing how to send a discovery UDP broadcast like I need to do?

Does it contain or point to example code showing how to send a discovery UDP broadcast like I need to do?

Probably not, given that you’re using a third-party development environment. You’ll have to adapt the concepts and snippets in those posts to your tooling.

Share and Enjoy

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

Actually the MAUI team has asked for examples of native code. I assume that would be Obj-C or Swift.

The probably want to compare it to what they are doing right now.

If you have any examples in either Obj-C or Swift, please point me to them.

There are basic send and receive examples in Extra-ordinary Networking > Broadcasts and Multicasts, Hints and Tips.

There’s an example of how to iterate through the interface list in Extra-ordinary Networking > Network Interface APIs. It also shows how to get the interface’s legacy and functional type.

Actually the MAUI team has asked for examples of native code.

And you should feel free to invite them to contribute to this thread. I love helping tools vendors because it’s a significant force multiplier.

Share and Enjoy

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

Thank you.

I have invited them to this thread and will remind them.

@tpitman just curios, did you manage to get anywhere with this with the dotnet team? I am having a very similar issue.

UDP Broadcast on iOS18
 
 
Q