SSDP discovery not working? What am I doing wrong or is it not supported at all?

Hey! I have been trying the last few days to discover devices in my network using NWConnection and NWConnectionGroup by sending a SSDP packet to 239.255.255.250 on port 1900 (by definition of SSDP?). I already have the multicast entitlement but that made no difference. Here are the two approaches I have tried:

Code Block
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
let params: NWParameters = .udp
params.allowLocalEndpointReuse = true
let connection = NWConnection(host: "239.255.255.250", port: 1_900, using: params)
connection.stateUpdateHandler = { [weak viewController = self] state in
guard let viewController = viewController else {
return
}
switch state {
case .ready:
viewController.send(to: connection)
default: return
}
}
connection.receiveMessage { (data, context, isComplete, errir) in
if let data = data {
let dataString = String(data: data, encoding: .utf8)
print(dataString ?? "", isComplete)
} else {
print(data ?? "", isComplete)
}
}
connection.start(queue: .main)
}
private func send(to connection: NWConnection) {
let message = "M-SEARCH * HTTP/1.1\r\nSt: ssdp:all\r\nHost: 239.255.255.250:1900\r\nMan: \"ssdp:discover\"\r\nMx: 1\r\n\r\n"
let payload = message.data(using: .utf8)
connection.send(content: payload, completion: .contentProcessed { (error) in
if let error = error {
print(error.localizedDescription)
} else {
print("ok")
}
})
}
}

Code Block
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
do {
try setupConnection()
} catch {
print(error.localizedDescription)
}
}
private func setupConnection() throws {
let endpoints: [NWEndpoint] = [
.hostPort(host: "239.255.255.250", port: 1_900)
]
let params: NWParameters = .udp
params.allowLocalEndpointReuse = true
let multicast = try NWMulticastGroup(for: endpoints)
let group = NWConnectionGroup(with: multicast, using: params)
group.setReceiveHandler { (message, data, isComplete) in
if let data = data {
let dataString = String(data: data, encoding: .utf8)
print(message, dataString ?? "", isComplete)
} else {
print(message, data ?? "", isComplete)
}
}
group.stateUpdateHandler = { [weak viewController = self] (state) in
guard let viewController = viewController else {
return
}
switch state {
case .ready:
viewController.send(to: group)
default: return
}
}
group.start(queue: .main)
}
private func send(to group: NWConnectionGroup) {
let message = "M-SEARCH * HTTP/1.1\r\nST: ssdp:all\r\nHOST: 239.255.255.250:1900\r\nMAN: \"ssdp:discover\"\r\nMX: 1\r\n\r\n"
let payload = message.data(using: .utf8)
group.send(content: payload) { error in
if let error = error {
print(error.localizedDescription)
} else {
print("ok")
}
}
}
}

I hope to get some help here as I couldn't find any working examples online. Thanks in advance!

I already have the multicast entitlement

The first step here is to make sure that your have enabled this entitlement correctly. Build your app and then do the following:

Code Block
% codesign -d --entitlements :- /path/to/your.app
% security CMS -D -i /path/to/your.app/embedded.mobileprovision


Make sure com.apple.developer.networking.multicast, with a value of true, appears in both these property lists.

Second, your first approach is not the way forward. NWConnection is not rated for multicast or broadcast work.

Third, in your NWConnectionGroup code you don’t have anything that keeps the group object in memory. When setupConnection ends that releases the last reference to group which may then be deallocated, and it’ll cancel itself on deallocation. You must keep a strong reference to group around, typically as a property in the object that manages your networking.

Finally, if things still don’t work, I recommend that you use a packet trace (see Recording a Packet Trace) to determine whether the problem is on the send side, the remote side, or the receive side. That is:
  • Are you unable to send multicasts?

  • Do you send them but the remote peer doesn’t respond?

  • Does the remote peer respond but you don’t receive that response?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
Hey Quinn! Thank you for your answer!

I tried running both commands you provided.
First one returns the plist with my entitlement, so that's fine.

Archived project has the embedded.mobileprovision file which also includes the entitlement.

Assume NWConnectionGroup it is strong referenced and not deallocated going forward.

Also I get messages from devices in the network when they publish themselves but not when I send my package. I tried packet capturing and I can confirm that messages are being sent and my router responds with the response.

It works with a different app from the App-Store, they are using the upnpx library. Using the open source library BlueSocket works as-well, but I am trying to stick to the Network framework.

Data captured from reference app:
Code Block
15:42:35.873001 IP 192.168.1.47.59611 > 239.255.255.250.1900: UDP, length 157
15:42:35.927494 IP 192.168.1.1.1900 > 192.168.1.47.59611: UDP, length 438
15:42:35.927501 IP 192.168.1.1.1900 > 192.168.1.47.59611: UDP, length 406
15:42:35.927503 IP 192.168.1.1.1900 > 192.168.1.47.59611: UDP, length 426
15:42:35.927504 IP 192.168.1.1.1900 > 192.168.1.47.59611: UDP, length 430
15:42:35.927505 IP 192.168.1.1.1900 > 192.168.1.47.59611: UDP, length 358
15:42:35.927506 IP 192.168.1.1.1900 > 192.168.1.47.59611: UDP, length 367
15:42:35.927507 IP 192.168.1.1.1900 > 192.168.1.47.59611: UDP, length 367
15:42:35.927508 IP 192.168.1.1.1900 > 192.168.1.47.59611: UDP, length 367
15:42:35.927510 IP 192.168.1.1.1900 > 192.168.1.47.59611: UDP, length 422
15:42:35.927511 IP 192.168.1.1.1900 > 192.168.1.47.59611: UDP, length 422
15:42:35.927512 IP 192.168.1.1.1900 > 192.168.1.47.59611: UDP, length 420


Data captured from my app:
Code Block
15:42:53.975416 IP 192.168.1.47.57367 > 239.255.255.250.1900: UDP, length 94
15:42:54.032783 IP 192.168.1.1.1900 > 192.168.1.47.57367: UDP, length 367
15:42:54.032788 IP 192.168.1.1.1900 > 192.168.1.47.57367: UDP, length 367
15:42:54.032790 IP 192.168.1.1.1900 > 192.168.1.47.57367: UDP, length 367
15:42:54.032791 IP 192.168.1.1.1900 > 192.168.1.47.57367: UDP, length 422
15:42:54.032793 IP 192.168.1.1.1900 > 192.168.1.47.57367: UDP, length 422
15:42:54.032794 IP 192.168.1.1.1900 > 192.168.1.47.57367: UDP, length 420
15:42:54.032795 IP 192.168.1.1.1900 > 192.168.1.47.57367: UDP, length 438
15:42:54.032796 IP 192.168.1.1.1900 > 192.168.1.47.57367: UDP, length 406
15:42:54.032798 IP 192.168.1.1.1900 > 192.168.1.47.57367: UDP, length 426
15:42:54.032800 IP 192.168.1.1.1900 > 192.168.1.47.57367: UDP, length 430
15:42:54.032801 IP 192.168.1.1.1900 > 192.168.1.47.57367: UDP, length 358

  • According to the logs, I am able to send multicast packets

  • Seems like remote peer does respond

  • I do, for some reason, do not receive the response

Data captured from my app:

I’m not sure how to read that trace. Which IP address is your iOS device? And which IP address is your accessory?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
192.168.1.47 is my device, Network opens a random port, 57367 in this case, and sends an UDP packet with length of 94 (my SSDP request) to 239.255.255.250:1900. My Router, 192.168.1.1, forwards any UDP response from devices within the network to my iOS device at 192.168.1.47:57367.

So sending SSDP works, devices within the network respond, messages are being forwarded to my iOS device but it seems like Network does not handle received messages?

it seems like Network does not handle received messages?

Right, but that makes sense because the responses are unicast not multicast. I’m not sure how best to handle this situation. I’m going to ping Matt to see if he’s encountered it before. If not, we’ll likely recommend that you open a DTS tech support incident so that one of us can research this properly.

Share and Enjoy

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

What you are describing here is a known issue at the moment with NWConnectionGroup sending a multicast packets out and then expecting a unicast response back to the sender. This type of situation really comes to the forefront when using a protocol like SSDP. The issue here is that the unicast responses are in fact being sent back to the sender when using reply() but they are not picked by the original sender's internal receive. There are a few known bugs for this. One of them is (r. 74750057).

One known workaround here is to just respond to the entire group and then indicate via SSDP where your response is coming from. I understand that this is not the best workaround for SSDP, but it does work at the moment.

Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com

One known workaround here is to just respond to the entire group and then indicate via SSDP where your response is coming from. I understand that this is not the best workaround for SSDP, but it does work at the moment.

Hey Matt! Thank you for your answer and time!

If I understood you correctly, you are suggesting the devices in the network to respond to the group instead sending back responses with unicast. If that is the case, then this is not a viable workaround for me, as I have no control over the devices within somebody's network. If that is not what you meant, then I would kindly ask for a snippet showcasing the workaround.

Also the Network framework does not seem to be open source anymore? At least the ones I have found are a few years old.

I’ll leave you in Matt’s capable hands when it comes to NWConnectionGroup, but I can answer this:

Also the Network framework does not seem to be open source anymore?

Network framework is not, and has never been, open source. I’m not sure what you found, but it’s not Network framework.

Share and Enjoy

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

If I understood you correctly, you are suggesting the devices in the network to respond to the group instead sending back responses with unicast.

Yes, this was the only workaround I could develop here. Unfortunately that would have all devices in the group/network receive the response.

Here were some of the test cases I ran:

group?.setReceiveHandler(maximumMessageSize: 16384, rejectOversizedMessages: false) { (message, content, isComplete) in

    if let contentData = content,
        let strMessage = String(bytes: contentData, encoding: .utf8) {

        let sendContent = "HTTP/1.1 200 OK\r\n" +
                          "Cache-Control: max-age=60\r\n" +
                          "Content-Length: 0\r\n" +
                          "EXT:\r\n" +
                          "ST: upnp:rootdevice\r\n" +
                          "SERVER: \"iPhone 11 Pro\" \r\n" +
                          "USN: uuid:xxxx::upnp:rootdevice\r\n" +
                          "LOCATION: http://x.x.x.x:9090/my/api \r\n\r\n"
        

        // 1) ### Does not work to send directly with the extracted endpoint
        
        guard let remoteEndoint = message.remoteEndpoint else {
            return
        }
        
        let sendData = Data(sendContent.utf8)
        self.group?.send(content: sendData, to: remoteEndoint, message: message, completion: { (error) in
        })
        
        // 2) ### Does work but sends to all
        self.sendData(sendString: sendContent)
        
        // 3) ### Does not work to reply with a unicast directly on the inbound message
        let sendData = Data(sendContent.utf8)
        message.reply(content: sendData, message: .default)


        // 4) ### Does not work to use extractConnection() and build a new NWConnection. 
        // Resulted in connection setup failures
                    
        if let remoteEndpoint = message.remoteEndpoint,
          let existingConnection = self.connections[remoteEndpoint] {
            // Use existing connection
            existingConnection.sendDataOnConnection(connectionMessage: sendContent)

        } else if let connection = message.extractConnection(),
          let remoteEndpoint = message.remoteEndpoint {
            // Create new connection
            let conn = NetworkConnection(newConnection: connection)
            conn.delegate = self
            conn.startConnection()
            self.connections[remoteEndpoint] = conn
        } else if let remoteEndpoint = message.remoteEndpoint {
            // New connection with endpoint

            let conn = NetworkConnection(endpoint: remoteEndpoint)
            conn.delegate = self
            conn.startConnection()
            self.connections[remoteEndpoint] = conn
        }

    }
}


func sendData(sendString: String) {
    let groupSendContent = Data(sendString.utf8)
    group?.send(content: groupSendContent) { (error) in
        // proceed 
    }
}

Most notably it was concluded that a group should be able to run message.reply() here to unicast back to the sender. I would open a bug report on this for your use-case as well.

Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com

I reported a bug (9120983) for this and I really hope this gets resolved (soon-ish). In the meantime I might use a library that supports this.

Thank you both, Quinn and Matt, for your time and help.

I reported a bug (9120983) for this

Thank you for opening the bug. I copied myself on it internally and added some extra notes to the previous bug I mentioned (r. 74750057).

Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com

Hey guys!

It seems I just stumbled upon the exact same issue whilst developing a multiplatform (iOS and macOS) app that would use SSDP (and potentially other multicast-based protocols) to discover devices on the user's network. To ensure I'd follow Apple's guidelines as best as possible, I decided to go for the Network framework instead of another one (didn't see this post before I started).

Unfortunately, this still seems to be an issue. Please see my playground code below for testing. I never get to see any response from clients, but Wireshark and TCPDump clearly show the outgoing and incoming packets.

The code is ready to be dumped into Playground:

import Foundation
import Network

final class SSDPBrowser {    

    private var queue = DispatchQueue.global(qos: .userInitiated)
    private var listener: NWListener?
    private var listeningPort: NWEndpoint.Port = 0
    private var connection: NWConnection?
    private var ssdpIP: NWEndpoint.Host = "239.255.255.250"
    private var ssdpPort: NWEndpoint.Port = 1_900
    
    public func connect() {
        self.connection = NWConnection(host: self.ssdpIP, port: self.ssdpPort, using: .udp)

        connection!.stateUpdateHandler = { (newState) in
            switch (newState) {
            case .preparing:
                print("Entered state: preparing")
            case .ready:
                print("Entered state: ready")
            case .setup:
                print("Entered state: setup")
            case .cancelled:
                print("Entered state: cancelled")
            case .waiting:
                print("Entered state: waiting")
            case .failed:
                print("Entered state: failed")
            default:
                print("Entered an unknown state")
            }
        }

        connection!.viabilityUpdateHandler = { (isViable) in
            if (isViable) {
                print("Connection is viable")
            } else {
                print("Connection is not viable")
            }
        }

        connection!.betterPathUpdateHandler = { (betterPathAvailable) in
            if (betterPathAvailable) {
                print("A better path is availble")
            } else {
                print("No better path is available")
            }
        }
        connection!.start(queue: .global())
    }

    public func sendSSDPDiscoveryPacket() {

        let message = "M-SEARCH * HTTP/1.1\r\n" +
                    "MAN: \"ssdp:discover\"\r\n" +
                    "HOST: \(self.ssdpIP):\(self.ssdpPort)\r\n" +
                    "ST: \"ssdp:all\"\r\n" +
                    "MX: 10\r\n\r\n"
        let payload = message.data(using: .utf8)

        self.connection!.send(content: payload, completion: .contentProcessed({ sendError in
            if let error = sendError {
                print("Unable to process and send data: \(error)")
            } else {
                print("SSDP discovery packet has been sent:\r---\r\(message)")
                self.connection!.receiveMessage { (data, context, isComplete, error) in
                    if let unwrappedError = error {
                        print("Error: NWError received in \(#function) - \(unwrappedError)")
                        return
                    }
                    guard isComplete, let data = data else {
                        print("Error: Received nil Data with context - \(String(describing: context))")
                        return
                    }
                    print("Received message: " + String(decoding: data, as: UTF8.self))
                }
            }
        }))
    }
}

let ssdpBrowser = SSDPBrowser()
ssdpBrowser.connect()
ssdpBrowser.sendSSDPDiscoveryPacket()

As a workaround, I went ahead and tried to test another framework based on Bluesockets called SSDPClient (https://github.com/resourcepool/ssdp-client), but that didn't work either. It seems the receive fails for some unknown reason (error 9982). Not even the basic example they propose on Github seems to work under Xcode 13 anymore.

Do you have any update for us if and how things evolved with Bug 9120983, or maybe any other hint on what I can do to overcome this?

Many thanks for your support in advance, Nicolas

The bug mentioned by Matt upthread (r. 74750057) was fixed in iOS 15.

Share and Enjoy

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

@eskimo - would you know what version of macOS first includes the same fix?

AFAICT this fix was included in the macOS release aligned with iOS 15, that is, macOS 12.

Share and Enjoy

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

SSDP discovery not working? What am I doing wrong or is it not supported at all?
 
 
Q