Not getting UDP broadcast responses

I'm attempting to build an app the broadcasts on the local network and awaits responses to those broadcast requests. However, the app does not receive the responses. Essentially, the app broadcasts "DISCOVER:1000" to 10.11.21.255, and expects "ADDRESS:10.11.21.100", but never receives it. I built a test client in python, and it works as expected. Running tcpdump on the server shows the response being sent by the server. It just never reaches the ios app.

In case it matters, I have the multicast entitlement for the app and local network enabled in Info.plist.

import Foundation
import Network

class UDPClient: ObservableObject {
    private var connection: NWConnection?
    private let networkQueue = DispatchQueue(label: "com.example.udp")
    @Published var isReady = false
    private var isListening = false
    
    func connect() {
        let host = "10.11.21.255"
        let port = UInt16(12345)
        
        let endpoint = NWEndpoint.hostPort(host: NWEndpoint.Host(host), port: NWEndpoint.Port(integerLiteral: port))
        connection = NWConnection(to: endpoint, using: .udp)
        
        connection?.stateUpdateHandler = { [weak self] state in
            switch state {
            case .ready:
                self?.networkQueue.async {
                    print("Connection ready")
                    self?.startReceiving()
                    DispatchQueue.main.async {
                        self?.isReady = true
                    }
                }
            case .failed(let error):
                print("Connection failed: \(error)")
                DispatchQueue.main.async {
                    self?.isReady = false
                }
            case .waiting(let error):
                print("Connection waiting: \(error)")
                DispatchQueue.main.async {
                    self?.isReady = false
                }
            default:
                DispatchQueue.main.async {
                    self?.isReady = false
                }
                break
            }
        }
        
        connection?.start(queue: networkQueue)
    }
    
    func send() {
        let message = "DISCOVER:1000"
        guard let data = message.data(using: .utf8) else {
            print("Failed to convert message to data")
            return
        }
        
        guard let connection = self.connection else {
            return
        }
        
        networkQueue.async { [weak self] in
            print("Attempting to send message...")
            
            guard let self = self else { return }
            
            if self.isReady {
                // Ensure we're listening before sending
                if !self.isListening {
                    self.startReceiving()
                    // Add a small delay to ensure the receiver is ready
                    self.networkQueue.asyncAfter(deadline: .now() + 0.1) {
                        self.performSend(data: data, connection: connection)
                    }
                } else {
                    self.performSend(data: data, connection: connection)
                }
            } else {
                print("Connection is not ready. Retrying in 100ms...")
                self.networkQueue.asyncAfter(deadline: .now() + 0.1) {
                    self.send()
                }
            }
        }
    }
    
    private func performSend(data: Data, connection: NWConnection) {
        connection.send(content: data, completion: .contentProcessed { error in
            if let error = error {
                print("Failed to send: \(error)")
            } else {
                print("Send completed successfully")
            }
        })
    }
    
    private func startReceiving() {
        print("Starting to receive messages...")
        isListening = true
        connection?.receiveMessage { [weak self] content, context, isComplete, error in
            guard let self = self else { return }
            
            if let error = error {
                print("Receive error: \(error)")
                return
            }
            
            if let data = content {
                print("Received data: \(data)")
                if let responseString = String(data: data, encoding: .utf8) {
                    print("Received response: \(responseString)")
                } else {
                    print("Received data could not be converted to string.")
                }
            } else {
                print("No data received.")
            }
            
            // Continue receiving
            self.startReceiving()
        }
    }
    
    func disconnect() {
        networkQueue.async { [weak self] in
            self?.connection?.cancel()
            self?.isListening = false
            DispatchQueue.main.async {
                self?.isReady = false
            }
            print("Disconnected")
        }
    }
}

My main view:

import SwiftUI

struct ContentView: View {
    @StateObject private var udpClient = UDPClient()
    
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, world!")
        }
        .padding()
        .onAppear() {
            udpClient.connect()
            udpClient.send()
        }
    }
}

#Preview {
    ContentView()
}
Answered by DTS Engineer in 816426022

Network framework’s support for UDP broadcasts is quite limited. This is something I call out in TN3151 Choosing the right networking API.

It’s best to think of an NWConnection like a connected UDP socket. It’s really intended to be used to transfer a UDP flow, that is, a series of datagrams between the same local IP / local port / remote IP / remote port tuple. If your network protocol needs to do something else, it’s unlikely that NWConnection will help.

The standard fallback is BSD Sockets.

However, before you go down that path I’d want to check whether you really need to be operating at this level. It looks like you’re implementing a service discovery protocol, and if that’s the case then Bonjour is a much better option. I talk about this in Extra-ordinary Networking > Don’t Try to Get the Device’s IP Address > Service Discovery.

Share and Enjoy

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

Small update. I changed the ip address of the initial broadcast message to the server so that it unicasts, and it works. Does ios handle broadcast udp differently?

func connect() {
        let host = "10.11.21.100"

More research. Could this be the issue?

https://developer.apple.com/forums/thread/685690

If the reply is coming from the server (10.11.21.100) and the ip doesn't match the broadcast ip (10.11.21.255), could NW be ignoring it?

Accepted Answer

Network framework’s support for UDP broadcasts is quite limited. This is something I call out in TN3151 Choosing the right networking API.

It’s best to think of an NWConnection like a connected UDP socket. It’s really intended to be used to transfer a UDP flow, that is, a series of datagrams between the same local IP / local port / remote IP / remote port tuple. If your network protocol needs to do something else, it’s unlikely that NWConnection will help.

The standard fallback is BSD Sockets.

However, before you go down that path I’d want to check whether you really need to be operating at this level. It looks like you’re implementing a service discovery protocol, and if that’s the case then Bonjour is a much better option. I talk about this in Extra-ordinary Networking > Don’t Try to Get the Device’s IP Address > Service Discovery.

Share and Enjoy

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

Not getting UDP broadcast responses
 
 
Q