Unable to read broadcast UDP packets without blocking others

Hi all

The situation is as follows.

  • I have a device that I connect to via WiFi, this device amongst other things broadcasts a stream of UDP data that I want to read in my App.
  • Other Apps on the device (written by other developers) will also want to read these packets.
  • I want to be able to receive the broadcast UDP messages without blocking other Apps from doing so.

The problem I have encountered is I seem to be fetching the data in a way that is not playing nice with others.

  • If I start the other apps first, they can receive data, and when I connect on my App I am able to receive the UDP messages using NWListener generated connections, or using socket/bind via getaddrinfo or just directly creating a sockaddr_in
  • If I start my App and connect first, other Apps are not then able to retrieve any data, I presume because I am binding to the port somehow and hogging it.

I have tried setting SO_REUSEADDR, SO_REUSEPORT on the socket, with no success.

I have not been able to receive any UDP data using recvfrom etc unless I do bind, however from my research I was lead to believe I shouldn't need to do bind when just listening for udp broadcast data.

When using getifaddrs to view the various connections available to the App, I can see the device as follows.

Name: en0
Is Broadcast (checking ifa_flags & IFF_BROADCAST)
addr: 192.168.4.2
netmask: 255.255.255.0
gateway: 192.168.4.255

If I connect the WiFi device to my Mac, I can use tcdump to see the udp packets being broadcast as follows.

tcpdump -c 4 -l -n -i en0 'udp and port 4000'
Password:
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on en0, link-type EN10MB (Ethernet), snapshot length 524288 bytes
11:47:27.297327 IP 192.168.4.1.49153 > 192.168.4.3.4000: UDP, length 33
11:47:27.429211 IP 192.168.4.1.49153 > 192.168.4.3.4000: UDP, length 75
11:47:27.911906 IP 192.168.4.1.49153 > 192.168.4.3.4000: UDP, length 52
11:47:28.029487 IP 192.168.4.1.49153 > 192.168.4.3.4000: UDP, length 33
4 packets captured
8 packets received by filter
0 packets dropped by kernel

I feel like I have all the pieces, but I don't understand the correct process for setting up a socket to receive broadcast UDP packets from my device in a way that doesn't affect other Apps running on the device. I am confident to try solutions either using NWListener/NWConnection solutions or BSD Sockets.

I’d like to clarify some terminology here. You wrote:

  • I have a device that I connect to via WiFi, this device amongst other things broadcasts a stream of UDP data that I want to read in my App.

  • Other Apps on the device (written by other developers) will also want to read these packets.

These bullets both use device but I think you mean different things by that:

  • In the first bullet, device means some sort of Wi-Fi connected hardware that’s not from Apple.

  • In the second, device means an iOS device.

Right?

FYI, in Apple Speak™ we get around this confusion by using accessory for the first case.

my research I was lead to believe I shouldn't need to do bind when just listening for udp broadcast data.

I don’t understand this. To receive a UDP broadcast you must bind in order to specify the UDP port.

I’ll have more to sense once I get your answer to the above.

Share and Enjoy

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

Yes, you are correct, sorry for the confusing terminology.

I have a WiFi accessory that I connect to, this broadcasts UDP packets.
Several Apps on the device are interested in reading these UDP packets, users often want to run these apps side by side on their iPads (both running in foreground).

I am writing what is essentially a client that just consumes these UDP packets.
There are other clients running on the same device.
If those clients are allowed to run and connect first, they are able to consume the UDP packets and show the associated data. I am also able to consume the UDP data.
If I run and connect first, I am able to consume the UDP data, but the other clients are not able to consume the UDP packets.
If I don't run my app, but run two other clients, they seem to both be able to consume the UDP packets.

I have the following code fetching UDP data, it works as I would expect on my iPad apart from how it seems to block others. Also apologies some of the comments are out of date due to how I have tried various things to resolve the issue. But, I have confirmed the following code runs and fetches data.

    /// Fetch the UDP socket address info for the supplied port.
    var hints: addrinfo = addrinfo(
        ai_flags: AI_PASSIVE,
        ai_family: Int32(AF_INET),
        ai_socktype: SOCK_DGRAM,
        ai_protocol: 0,
        ai_addrlen: 0,
        ai_canonname: nil,
        ai_addr: nil,
        ai_next: nil
    )
    
    
    var servinfo: UnsafeMutablePointer<addrinfo>? = nil
    let rv = getaddrinfo(nil, "\(4000)", &hints, &servinfo)
    if(rv != 0) {
        if let s = gai_strerror(rv) {
            print("getaddreinfo error: %s\n", String(cString: s))
        }
        endWith(error: ClientError.CouldNotFetchAdresses)
        return
    }

    //iterate through the returned addresses and try to connect to one.
    var sockfd: Int32 = -1
    var servinfoi: UnsafeMutablePointer<addrinfo>? = servinfo
    while servinfoi != nil {
        
        if let si = servinfo {
            sockfd = socket(si.pointee.ai_family, si.pointee.ai_socktype, si.pointee.ai_protocol)
            if sockfd == -1 {
                print("listener: socket could not be created. iterate to next")
                servinfoi = servinfoi?.pointee.ai_next
                continue
            }
             
            //set the port to play nicely with other apps/sockets
            var resuse: Int = 1
            setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &resuse, socklen_t(MemoryLayout<Int>.size))
            setsockopt(sockfd, SOL_SOCKET, SO_REUSEPORT, &resuse, socklen_t(MemoryLayout<Int>.size))
            
            let bound = bind(sockfd, si.pointee.ai_addr, si.pointee.ai_addrlen)
            if bound == -1 {
                close(sockfd)
                print("listener: bound call could not be handled. iterate to next")
                servinfoi = servinfoi?.pointee.ai_next
                continue
            }
            
            //enable the nonblock flag on the socket so we can use poll to avoid locking up resources
            var flags: Int32 = 0;
            flags = fcntl(sockfd,F_GETFL,0);
            _ = fcntl(sockfd, F_SETFL, flags | O_NONBLOCK)
            
            break
        }
    }

    
    if (servinfoi == nil) {
        print("listener: failed to bind socket\n")
        endWith(error: ClientError.CouldNotBindSocket)
        return
    }

    freeaddrinfo(servinfo);
    
    print("connected")
    
    //as long as we can, we continue to read from the socket
    while continueToFetchMessages {
        
        //we use poll with a timeout of 500ms, so the loop will sleep here until the
        //OS tells us there is more data, or the timeout happens. so on entry to background
        //at worst the loop exits after 500ms.
        var pollInfo: pollfd = pollfd(fd: sockfd, events: Int16(POLLIN), revents: 0)
        let poll_count = poll(&pollInfo, 1, 1)
        
        //if we had an error trying to poll, we want to stop the fetch loop which will close the socket.
        if(poll_count == -1) {
            close(sockfd)
            endWith(error: ClientError.CouldNotPollSocket)
            return
        }
        
        //if we have info read to read, we can do so.
        //if this is false it probably means no data arrived in the allotted 500ms block
        //so we just loop around and try again
        if((pollInfo.revents & Int16(POLLIN)) != 0) {
            
            var numbytes: Int = -1
            var buffer: Data = .init(count: 256)
            buffer.withUnsafeMutableBytes { mbp in
                numbytes = recv(sockfd, mbp.baseAddress, 255, 0)
            }
            
            //it is apparently valid to read a zero byte UDP packet, so we want to check that we got something more than that before we handle
            if numbytes > 0 {
                let out = buffer.subdata(in: buffer.startIndex ..< buffer.startIndex.advanced(by: numbytes))
                DispatchQueue.main.async {
                    let d = Message().messageType(data: out)
                    self.messageCallback(d, out)
                }
            }
        }
    }
    
    //remember to close the socket when the loop bails
    //this is what permits other apps to use the resource
    close(sockfd)

users often want to run these apps side by side on their iPads

Ah, that’s a key point that I was going to ask about.

ISTR an issue like this with Network framework but AFAIK this should work for BSD Sockets.

I like your use of getaddrinfo here. I’ve come to the same conclusion as you, that this is the only reasonable way to deal with BSD Sockets addresses in Swift.

However, it does obfuscate the address a little. What address do you actually bind to? If you feed servinfo into getnameinfo, specifying NI_NUMERICHOST and NI_NUMERICSERV, what do you get back?

Share and Enjoy

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

I think I've done this correctly, the following code has been inserted outside the while loop that creates the socket and binds.

    if let info = servinfoi?.pointee.ai_addr {
        var hbuf: [CChar] = Array(repeating: 0, count: Int(NI_MAXHOST))
        var sbuf: [CChar] = Array(repeating: 0, count: Int(NI_MAXSERV))
        let flag = NI_NUMERICHOST | NI_NUMERICSERV
        let nameInfoResult = getnameinfo(info, socklen_t(info.pointee.sa_len), &hbuf, socklen_t(NI_MAXHOST), &sbuf, socklen_t(NI_MAXSERV), flag)
        if nameInfoResult == 0 {
            print("Host: \(String(cString: hbuf)), Serv: \(String(cString: sbuf))")
        }
    }

The result is

Host: 0.0.0.0, Serv: 4000

OK, that’s what I was expecting.

So this is a weird one. I’ve got no good theories as to what’s going wrong.

If you put your UDP listener code into two test apps and run them both simultaneously, do you see the problem?

Share and Enjoy

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

If you put your UDP listener code into two test apps and run them both simultaneously, do you see the problem

Yep, the problem still exists with the above code. The first App to connect will receive the data as expected.
The second App will not get any error messages, it will bind to the 0.0.0.0 4000 address, but it will not get any data.
The poll call is never indicating there is anything to read (eg, pollInfo.revents is always 0 ).

OK, you should definitely file a bug about this.

Please post your bug number, just for the record.

Beyond that, I’m going to recommend that you also open a DTS tech support incident so that I can help you explore a solution. We know that’s possible because, as you’ve noted, other apps don’t have this problem.

Share and Enjoy

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

Report filed at FB12200645 (Two Apps running in the foreground side by side cannot both read broadcast UDP packets from an external source at the same time via BSD sockets.)

I think I did that right, been a while.

I will open the direct tech support issue.

Unable to read broadcast UDP packets without blocking others
 
 
Q