NWConnection UDP Broadcast not sent out correctly

Problem

I am trying to send out a broadcast using NWConnection and then listen for responses using NWListener on port 50913. Although the broadcast is sent out correctly (= no error is thrown upon sending), I only get responses to my broadcast from what I suppose are the network interfaces of my own MacBook. In other words, it seems like the broadcast is never really submitted to the network.

Context

I don't have in-depth knowledge about the behavior of UDP which is why I am confused about this behavior. I've been reading online about this and couldn't find anything really related to the behavior I am experiencing. I've also looked at this developer forums entry and implemented the broadcast accordingly. The response from @meaton does not suggest that broadcasts are not supported by NWConnection (which is what I thought to be the culprit initially), and I am not getting the error they are talking about in their post, but a behavior that is entirely different.

Does anyone know what is wrong with my implementation?

Code

final public class BroadcastDiscoveryEngine {
    private let logger: Logger = Logger.init(for: BroadcastDiscoveryEngine.self)

    private let broadcastConnection: NWConnection
    private let broadcastResponseListener: NWListener
    private let responseParser: BroadcastResponseParser = BroadcastResponseParser()

    private var discoveryContinuation: AsyncStream<Discovery>.Continuation? = nil

    init() throws {
        let parameters = NWParameters.udp
        parameters.allowLocalEndpointReuse = true
        parameters.allowFastOpen = true
        parameters.includePeerToPeer = true

        broadcastConnection = NWConnection(host: .ipv4(.broadcast), port: .init(integerLiteral: 50913), using: parameters)
        broadcastResponseListener = try NWListener(using: parameters, on: 50913)
    }

    func startBroadcast(continuation: AsyncStream<Discovery>.Continuation) {
        discoveryContinuation = continuation
        broadcastConnection.stateUpdateHandler = handleBroadcastConnectionStateUpdate(state:)
        broadcastConnection.start(queue: .global(qos: .default))
        startBroadcastListener()
    }

    func stopBroadcast() {
        broadcastConnection.cancel()
        broadcastResponseListener.cancel()
    }

    private func sendBroadcastMessage() {
        broadcastConnection.send(content: "my_broadcast_message".data(using: .utf8), completion: .contentProcessed({ error in
            if let error = error {
                self.logger.error("Sending broadcast message failed with error: \(error.debugDescription, privacy: .public)")
                self.broadcastConnection.cancel()
                self.broadcastResponseListener.cancel()
            }
            self.logger.info("Broadcast message sent.")
        }))
    }

    private func handleBroadcastConnectionStateUpdate(state: NWConnection.State) {
        switch state {
        // shortened other cases since only logging occurs
        case .ready:
            logger.info("Broadcast connection established, ready to send and receive data.")
            sendBroadcastMessage()
        }
    }
}

extension BroadcastDiscoveryEngine {
    private func startBroadcastListener() {
        broadcastResponseListener.stateUpdateHandler = handleBroadcastResponseListenerStateUpdate(state:)
        broadcastResponseListener.newConnectionHandler = handleIncomingConnection(connection:)
        broadcastResponseListener.start(queue: .global(qos: .default))
    }

    private func handleBroadcastResponseListenerStateUpdate(state: NWListener.State) {
        switch state {
        // shortened cases since only logging occurs
        }
    }

    private func handleIncomingConnection(connection: NWConnection) {
        connection.stateUpdateHandler = { state in self.handleIncomingConnectionStateUpdate(connection: connection, state: state) }
        connection.start(queue: .global(qos: .default))
    }

    private func handleIncomingConnectionStateUpdate(connection: NWConnection, state: NWConnection.State) {
        switch state {
        // shortened other cases since only logging occurs
        case .ready:
            logger.info("Incoming connection (\(connection.debugDescription, privacy: .public) established, ready to send and receive data.")
            connection.receiveMessage { content, contentContext, isComplete, error in
                self.receiveBroadcastResponse(connection: connection, content: content, contentContext: contentContext, isComplete: isComplete, error: error)
            }
        }
    }

    private func receiveBroadcastResponse(connection: NWConnection, content: Data?, contentContext: NWConnection.ContentContext?, isComplete: Bool, error: NWError?) {
        // shortened: handles parsing accordingly and then cancels connection
        connection.cancel()
    }
}

Network framework has very limited support for UDP broadcasts; it’s quite possible that you’ll have to use BSD Sockets for this )-: However, before I send you off in that direction, I’d like to get a better understanding of your goal.

Let’s ignore APIs and focus on the network for the moment. UDP traffic is characterised by a tuple of local IP, local port, remote IP, remote port. Imagine your device’s Wi-Fi address is 192.168.1.37/24. What does this tuple look like for:

  • The datagram you want to send?

  • The datagrams you’re expecting to receive?

Also, what’s your high-level goal here? Some sort of service discovery? If so, can you switch to using the industry standard Bonjour protocols [1]?

Share and Enjoy

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

[1] Bonjour is an Apple term for:

Hey @eskimo :) Thanks for the response. Some more context: We are trying to discover our own devices in the network. For that, we use a variety of different discovery methods. Bonjour is one of them, alongside others. For certain network configurations, we have to use UDP Broadcasts as a fallback when e.g. mDNS is not available.

The datagram we send:

  • local ip: the ip of the sender
  • local port (where we have the listener): 50913
  • remote ip: broadcast (255.255.255.255)
  • remote port: 50913
  • datagram: some short identifier string that our devices respond to in a specific format (allowing us to connect to them right after)

The datagram we receive:

  • remote ip: the ip of our to-be-discovered device
  • remote port: 50913
  • local ip: the ip of the initial broadcast-sender
  • local port: 50913 (that's where the listener is running)
  • datagram: a response following a pre-defined format that allows us to filter which of the responding devices are ours and should be connected to

The use case is fixed and I cannot change anything about how we discover the devices. We have all the other discovery methods implemented and running, UDP broadcast is the last one that's missing.

I really appreciate your help and I hope this information is sufficient. Please let me know if I should clarify.

NWConnection UDP Broadcast not sent out correctly
 
 
Q