2

I have a question regarding how to set up a UDP listener on iOS 14. I have a UDP listener which has worked in the past, but after updating to iOS 14 it works sporadically/not at all.

This lives in an NSObject, and listens for a UDP broadcast across the local network on port 15000 (no specific IP address). It uses the CocoaAsyncSocket library. When I call setUpSocket() local network permissions are not triggered, but the app is able to sporadically pick up UDP packets.


var socket: GCDAsyncUdpSocket?
var broadcastPort: UInt16 = 15000
var broadcastAddress: String = ""
var connectAddress = ""
var connectPort = 0

func setUpSocket() {
    findUDP()
    let socket = GCDAsyncUdpSocket(delegate: self, delegateQueue: DispatchQueue.main)
     
    socket.setIPv4Enabled(true)
    socket.setIPv6Enabled(false)
     
    do {
      try socket.bind(toPort: broadcastPort) /*15000*/
      try socket.enableBroadcast(false)
      try socket.beginReceiving()
       
    } catch let error as NSError {
       
      print("Issue with setting up listener \(error)")
       
    }
     
  }

/*Called when UDP packets are received.*/
func udpSocket(_ sock: GCDAsyncUdpSocket, didReceive data: Data, fromAddress: Data, withFilterContext filterContext: Any?) {
     
    do {
      let jsonDictionary = try JSONSerialization.jsonObject(with: data, options: []) as! [String : Any]
       
      if (connected == false) {
        if (jsonDictionary["Addresses"] != nil) {
          if (jsonDictionary["Addresses"] is NSArray) {
            let addresses = jsonDictionary["Addresses"] as! NSArray
             
            for i in addresses {
              let ipAddress:String = i as! String
              if (ipAddress.range(of: "^([0-9]{1,3}\\.){3}[0-9]{1,3}(\\/([0-9]|[1-2][0-9]|3[0-2]))?$", options: .regularExpression) != nil) {
                connectAddress = ipAddress
              }
            }
            connectPort = jsonDictionary["Port"] as! Int
          }
           
          /*Sets up a TCP connection on the IP and Port provided in the UDP broadcast.*/
          setupNetworkCommunication(ip: connectAddress, port: connectPort)
          
          closeSocket()

        }
      }
       
    } catch let error {
      return print(error)
    }
  }

How can I update this to comply with iOS 14? If I need to update to use Bonjour services, how can I listen on a port without specifying an address (and without having to look for a specific Bonjour service broadcast, because the broadcast I'm looking for doesn't use Bonjour).

Is it acceptable to quickly open and close a Bonjour NWBrowser in order to trigger the network permissions, and then use my code as-is? This seems to work but seems hacky at best.

Thanks in advance.

tomthetank
  • 51
  • 1
  • 5
  • Same problem I am facing today. I have a unity app with UDP connection which was working fine in the lower version(13). But after I update the iPad to 14.0 it stops working – Koushik Sep 28 '20 at 06:42

2 Answers2

5

Here are the steps that we had to go through in order to use CocoaAsyncSocket with UDP in our app:

  1. Request the multicast entitlement from Apple (requestor must be the Account Holder of the team): com.apple.developer.networking.multicast

  2. In the target's Info.plist, set a string for the following key:

    Privacy - Local Network Usage Description

  3. After getting permission for the entitlement from Apple, add a true(1) Boolean value for the following key in the *.entitlements file for your app (this last step is what kept us from receiving UDP broadcast packets):

    com.apple.developer.networking.multicast

    *.entitlements file example

Jeremy Caney
  • 7,102
  • 69
  • 48
  • 77
  • Is there a way I could internal test my app on test devices without performing step 1? In step 1, the "Multicast Networking Entitlement Request Form" is asking for the App Store URL of the application and my app is not live. – Naresh Dec 29 '20 at 09:33
  • It depends. I was able to run in the debugger, but devices loaded from TestFlight did not work. – Stephen Lewis Dec 30 '20 at 16:04
  • Thank you @Stephen. I distribute my app using IPAs signed with Ad-Hoc profiles. I think they going to work the same as the TestFlight builds. – Naresh Jan 04 '21 at 05:08
  • How long did you have to wait for Apple to approve your request? – Mark Bridges Jun 11 '21 at 09:55
  • I think it took about a week. – Stephen Lewis Jun 12 '21 at 13:23
3

I was able to explore this some more and got some help via the apple developer forums, posting an answer here as well for those who are interested.

I ended up using an NWListener to listen for UDP packets, then set up an NWConnection once once I'd received something. I use this NWConnection to read data from the UDP broadcast.

From Quinn "The Eskimo:"

Listening for UDP broadcasts via an NWListener and then using the NWConnection objects it vends (via the new connection handler) to communicate over unicast with the broadcast’s sender is an expected use case.

I encourage anyone reading this to check out our discussion on the Apple Developer Forum as well.

Here is my implementation:

  var udpListener: NWListener?
  var udpConnection: NWConnection?
  var backgroundQueueUdpListener  = DispatchQueue.main
   
  func findUDP() {
    let params = NWParameters.udp
    udpListener = try? NWListener(using: params, on: 15000)
     
    udpListener?.service = NWListener.Service.init(type: "_appname._udp")
     
    self.udpListener?.stateUpdateHandler = { update in
      print("update")
      print(update)
      switch update {
      case .failed:
        print("failed")
      default:
        print("default update")
      }
    }
    self.udpListener?.newConnectionHandler = { connection in
      print("connection")
      print(connection)
      self.createConnection(connection: connection)
      self.udpListener?.cancel()
    }
    udpListener?.start(queue: self.backgroundQueueUdpListener)
  }
   
  func createConnection(connection: NWConnection) {
    self.udpConnection = connection
      self.udpConnection?.stateUpdateHandler = { (newState) in
        switch (newState) {
        case .ready:
          print("ready")
          self.send()
          self.receive()
        case .setup:
          print("setup")
        case .cancelled:
          print("cancelled")
        case .preparing:
          print("Preparing")
        default:
          print("waiting or failed")
        }
      }
      self.udpConnection?.start(queue: .global())
  }
   
  func endConnection() {
    self.udpConnection?.cancel()
  }
tomthetank
  • 51
  • 1
  • 5
  • I have the same issue. But replacing CocoaAsyncSocket with the Network framework did not fix my problem (in production). Is the Bonjour plist entry necessary? I believe yours is empty. – Mihai Popa Oct 21 '20 at 10:43
  • 1
    The Bonjour plist entry is necessary. Mine contains '_appname._udp'. @MihaiPopa. – tomthetank Nov 11 '20 at 18:19